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
;
51 use PVE
::RESTEnvironment
qw(log_warn);
54 if (!$ENV{PVE_GENERATING_DOCS
}) {
55 require PVE
::HA
::Env
::PVE2
;
56 import PVE
::HA
::Env
::PVE2
;
57 require PVE
::HA
::Config
;
58 import PVE
::HA
::Config
;
62 use base
qw(PVE::RESTHandler);
64 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.";
66 my $resolve_cdrom_alias = sub {
69 if (my $value = $param->{cdrom
}) {
70 $value .= ",media=cdrom" if $value !~ m/media=/;
71 $param->{ide2
} = $value;
72 delete $param->{cdrom
};
76 # Used in import-enabled API endpoints. Parses drives using the extended '_with_alloc' schema.
77 my $foreach_volume_with_alloc = sub {
78 my ($param, $func) = @_;
80 for my $opt (sort keys $param->%*) {
81 next if !PVE
::QemuServer
::is_valid_drivename
($opt);
83 my $drive = PVE
::QemuServer
::Drive
::parse_drive
($opt, $param->{$opt}, 1);
86 $func->($opt, $drive);
90 my $check_drive_param = sub {
91 my ($param, $storecfg, $extra_checks) = @_;
93 for my $opt (sort keys $param->%*) {
94 next if !PVE
::QemuServer
::is_valid_drivename
($opt);
96 my $drive = PVE
::QemuServer
::parse_drive
($opt, $param->{$opt}, 1);
97 raise_param_exc
({ $opt => "unable to parse drive options" }) if !$drive;
99 if ($drive->{'import-from'}) {
100 if ($drive->{file
} !~ $PVE::QemuServer
::Drive
::NEW_DISK_RE
|| $3 != 0) {
102 $opt => "'import-from' requires special syntax - ".
103 "use <storage ID>:0,import-from=<source>",
107 if ($opt eq 'efidisk0') {
108 for my $required (qw(efitype pre-enrolled-keys)) {
109 if (!defined($drive->{$required})) {
111 $opt => "need to specify '$required' when using 'import-from'",
115 } elsif ($opt eq 'tpmstate0') {
116 raise_param_exc
({ $opt => "need to specify 'version' when using 'import-from'" })
117 if !defined($drive->{version
});
121 PVE
::QemuServer
::cleanup_drive_path
($opt, $storecfg, $drive);
123 $extra_checks->($drive) if $extra_checks;
125 $param->{$opt} = PVE
::QemuServer
::print_drive
($drive, 1);
129 my $check_storage_access = sub {
130 my ($rpcenv, $authuser, $storecfg, $vmid, $settings, $default_storage) = @_;
132 $foreach_volume_with_alloc->($settings, sub {
133 my ($ds, $drive) = @_;
135 my $isCDROM = PVE
::QemuServer
::drive_is_cdrom
($drive);
137 my $volid = $drive->{file
};
138 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
140 if (!$volid || ($volid eq 'none' || $volid eq 'cloudinit' || (defined($volname) && $volname eq 'cloudinit'))) {
142 } elsif ($isCDROM && ($volid eq 'cdrom')) {
143 $rpcenv->check($authuser, "/", ['Sys.Console']);
144 } elsif (!$isCDROM && ($volid =~ $PVE::QemuServer
::Drive
::NEW_DISK_RE
)) {
145 my ($storeid, $size) = ($2 || $default_storage, $3);
146 die "no storage ID specified (and no default storage)\n" if !$storeid;
147 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
148 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
149 raise_param_exc
({ storage
=> "storage '$storeid' does not support vm images"})
150 if !$scfg->{content
}->{images
};
152 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $volid);
154 my ($vtype) = PVE
::Storage
::parse_volname
($storecfg, $volid);
155 raise_param_exc
({ $ds => "content type needs to be 'images' or 'iso'" })
156 if $vtype ne 'images' && $vtype ne 'iso';
160 if (my $src_image = $drive->{'import-from'}) {
162 if (PVE
::Storage
::parse_volume_id
($src_image, 1)) { # PVE-managed volume
163 (my $vtype, undef, $src_vmid) = PVE
::Storage
::parse_volname
($storecfg, $src_image);
164 raise_param_exc
({ $ds => "$src_image has wrong type '$vtype' - not an image" })
165 if $vtype ne 'images';
168 if ($src_vmid) { # might be actively used by VM and will be copied via clone_disk()
169 $rpcenv->check($authuser, "/vms/${src_vmid}", ['VM.Clone']);
171 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $src_image);
176 $rpcenv->check($authuser, "/storage/$settings->{vmstatestorage}", ['Datastore.AllocateSpace'])
177 if defined($settings->{vmstatestorage
});
180 my $check_storage_access_clone = sub {
181 my ($rpcenv, $authuser, $storecfg, $conf, $storage) = @_;
185 PVE
::QemuConfig-
>foreach_volume($conf, sub {
186 my ($ds, $drive) = @_;
188 my $isCDROM = PVE
::QemuServer
::drive_is_cdrom
($drive);
190 my $volid = $drive->{file
};
192 return if !$volid || $volid eq 'none';
195 if ($volid eq 'cdrom') {
196 $rpcenv->check($authuser, "/", ['Sys.Console']);
198 # we simply allow access
199 my ($sid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
200 my $scfg = PVE
::Storage
::storage_config
($storecfg, $sid);
201 $sharedvm = 0 if !$scfg->{shared
};
205 my ($sid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
206 my $scfg = PVE
::Storage
::storage_config
($storecfg, $sid);
207 $sharedvm = 0 if !$scfg->{shared
};
209 $sid = $storage if $storage;
210 $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
214 $rpcenv->check($authuser, "/storage/$conf->{vmstatestorage}", ['Datastore.AllocateSpace'])
215 if defined($conf->{vmstatestorage
});
220 my $check_storage_access_migrate = sub {
221 my ($rpcenv, $authuser, $storecfg, $storage, $node) = @_;
223 PVE
::Storage
::storage_check_enabled
($storecfg, $storage, $node);
225 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
227 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storage);
228 die "storage '$storage' does not support vm images\n"
229 if !$scfg->{content
}->{images
};
232 my $import_from_volid = sub {
233 my ($storecfg, $src_volid, $dest_info, $vollist) = @_;
235 die "could not get size of $src_volid\n"
236 if !PVE
::Storage
::volume_size_info
($storecfg, $src_volid, 10);
238 die "cannot import from cloudinit disk\n"
239 if PVE
::QemuServer
::Drive
::drive_is_cloudinit
({ file
=> $src_volid });
241 my $src_vmid = (PVE
::Storage
::parse_volname
($storecfg, $src_volid))[2];
243 my $src_vm_state = sub {
244 my $exists = $src_vmid && PVE
::Cluster
::get_vmlist
()->{ids
}->{$src_vmid} ?
1 : 0;
248 eval { PVE
::QemuConfig
::assert_config_exists_on_node
($src_vmid); };
249 die "owner VM $src_vmid not on local node\n" if $@;
250 $runs = PVE
::QemuServer
::Helpers
::vm_running_locally
($src_vmid) || 0;
253 return ($exists, $runs);
256 my ($src_vm_exists, $running) = $src_vm_state->();
258 die "cannot import from '$src_volid' - full clone feature is not supported\n"
259 if !PVE
::Storage
::volume_has_feature
($storecfg, 'copy', $src_volid, undef, $running);
262 my ($src_vm_exists_now, $running_now) = $src_vm_state->();
264 die "owner VM $src_vmid changed state unexpectedly\n"
265 if $src_vm_exists_now != $src_vm_exists || $running_now != $running;
267 my $src_conf = $src_vm_exists_now ? PVE
::QemuConfig-
>load_config($src_vmid) : {};
269 my $src_drive = { file
=> $src_volid };
271 PVE
::QemuConfig-
>foreach_volume($src_conf, sub {
272 my ($ds, $drive) = @_;
274 return if $src_drivename;
276 if ($drive->{file
} eq $src_volid) {
278 $src_drivename = $ds;
284 running
=> $running_now,
285 drivename
=> $src_drivename,
290 my ($src_storeid) = PVE
::Storage
::parse_volume_id
($src_volid);
292 return PVE
::QemuServer
::clone_disk
(
301 PVE
::Storage
::get_bandwidth_limit
('clone', [$src_storeid, $dest_info->{storage
}]),
307 $cloned = PVE
::QemuConfig-
>lock_config_full($src_vmid, 30, $clonefn);
308 } elsif ($src_vmid) {
309 $cloned = PVE
::QemuConfig-
>lock_config_shared($src_vmid, 30, $clonefn);
311 $cloned = $clonefn->();
314 return $cloned->@{qw(file size)};
317 # Note: $pool is only needed when creating a VM, because pool permissions
318 # are automatically inherited if VM already exists inside a pool.
319 my sub create_disks
: prototype($$$$$$$$$$) {
337 my $live_import_mapping = {};
340 my ($ds, $disk) = @_;
342 my $volid = $disk->{file
};
343 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
345 if (!$volid || $volid eq 'none' || $volid eq 'cdrom') {
346 delete $disk->{size
};
347 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
348 } elsif (defined($volname) && $volname eq 'cloudinit') {
349 $storeid = $storeid // $default_storage;
350 die "no storage ID specified (and no default storage)\n" if !$storeid;
353 my $ci_key = PVE
::QemuConfig-
>has_cloudinit($conf, $ds)
354 || PVE
::QemuConfig-
>has_cloudinit($conf->{pending
} || {}, $ds)
355 || PVE
::QemuConfig-
>has_cloudinit($res, $ds)
357 die "$ds - cloud-init drive is already attached at '$ci_key'\n";
360 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
361 my $name = "vm-$vmid-cloudinit";
365 $fmt = $disk->{format
} // "qcow2";
368 $fmt = $disk->{format
} // "raw";
371 # Initial disk created with 4 MB and aligned to 4MB on regeneration
372 my $ci_size = PVE
::QemuServer
::Cloudinit
::CLOUDINIT_DISK_SIZE
;
373 my $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid, $fmt, $name, $ci_size/1024);
374 $disk->{file
} = $volid;
375 $disk->{media
} = 'cdrom';
376 push @$vollist, $volid;
377 delete $disk->{format
}; # no longer needed
378 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
379 print "$ds: successfully created disk '$res->{$ds}'\n";
380 } elsif ($volid =~ $PVE::QemuServer
::Drive
::NEW_DISK_RE
) {
381 my ($storeid, $size) = ($2 || $default_storage, $3);
382 die "no storage ID specified (and no default storage)\n" if !$storeid;
384 $size = PVE
::Tools
::convert_size
($size, 'gb' => 'kb'); # vdisk_alloc uses kb
386 my $live_import = $is_live_import && $ds ne 'efidisk0';
387 my $needs_creation = 1;
389 if (my $source = delete $disk->{'import-from'}) {
392 $needs_creation = $live_import;
394 if (PVE
::Storage
::parse_volume_id
($source, 1)) { # PVE-managed volume
395 if ($live_import && $ds ne 'efidisk0') {
396 my $path = PVE
::Storage
::path
($storecfg, $source)
397 or die "failed to get a path for '$source'\n";
399 ($size, my $source_format) = PVE
::Storage
::file_size_info
($source);
400 die "could not get file size of $source\n" if !$size;
401 $live_import_mapping->{$ds} = {
403 format
=> $source_format,
410 format
=> $disk->{format
},
413 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($conf, $disk)
414 if $ds eq 'efidisk0';
416 ($dst_volid, $size) = eval {
417 $import_from_volid->($storecfg, $source, $dest_info, $vollist);
419 die "cannot import from '$source' - $@" if $@;
422 $source = PVE
::Storage
::abs_filesystem_path
($storecfg, $source, 1);
423 ($size, my $source_format) = PVE
::Storage
::file_size_info
($source);
424 die "could not get file size of $source\n" if !$size;
426 if ($live_import && $ds ne 'efidisk0') {
427 $live_import_mapping->{$ds} = {
429 format
=> $source_format,
432 (undef, $dst_volid) = PVE
::QemuServer
::ImportDisk
::do_import
(
438 format
=> $disk->{format
},
439 'skip-config-update' => 1,
442 push @$vollist, $dst_volid;
446 if ($needs_creation) {
447 $size = PVE
::Tools
::convert_size
($size, 'b' => 'kb'); # vdisk_alloc uses kb
449 $disk->{file
} = $dst_volid;
450 $disk->{size
} = $size;
451 delete $disk->{format
}; # no longer needed
452 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
456 if ($needs_creation) {
457 my $defformat = PVE
::Storage
::storage_default_format
($storecfg, $storeid);
458 my $fmt = $disk->{format
} || $defformat;
461 if ($ds eq 'efidisk0') {
462 my $smm = PVE
::QemuServer
::Machine
::machine_type_is_q35
($conf);
463 ($volid, $size) = PVE
::QemuServer
::create_efidisk
(
464 $storecfg, $storeid, $vmid, $fmt, $arch, $disk, $smm);
465 } elsif ($ds eq 'tpmstate0') {
466 # swtpm can only use raw volumes, and uses a fixed size
467 $size = PVE
::Tools
::convert_size
(PVE
::QemuServer
::Drive
::TPMSTATE_DISK_SIZE
, 'b' => 'kb');
468 $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid, "raw", undef, $size);
470 $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid, $fmt, undef, $size);
472 push @$vollist, $volid;
473 $disk->{file
} = $volid;
474 $disk->{size
} = PVE
::Tools
::convert_size
($size, 'kb' => 'b');
475 delete $disk->{format
}; # no longer needed
476 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
479 print "$ds: successfully created disk '$res->{$ds}'\n";
481 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $volid);
483 my ($vtype) = PVE
::Storage
::parse_volname
($storecfg, $volid);
484 die "cannot use volume $volid - content type needs to be 'images' or 'iso'"
485 if $vtype ne 'images' && $vtype ne 'iso';
487 if (PVE
::QemuServer
::Drive
::drive_is_cloudinit
($disk)) {
489 my $ci_key = PVE
::QemuConfig-
>has_cloudinit($conf, $ds)
490 || PVE
::QemuConfig-
>has_cloudinit($conf->{pending
} || {}, $ds)
491 || PVE
::QemuConfig-
>has_cloudinit($res, $ds)
493 die "$ds - cloud-init drive is already attached at '$ci_key'\n";
498 PVE
::Storage
::activate_volumes
($storecfg, [ $volid ]) if $storeid;
500 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid);
501 die "volume $volid does not exist\n" if !$size;
502 $disk->{size
} = $size;
504 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
508 eval { $foreach_volume_with_alloc->($settings, $code); };
510 # free allocated images on error
512 syslog
('err', "VM $vmid creating disks failed");
513 foreach my $volid (@$vollist) {
514 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
520 # don't return empty import mappings
521 $live_import_mapping = undef if !%$live_import_mapping;
523 return ($vollist, $res, $live_import_mapping);
526 my $check_cpu_model_access = sub {
527 my ($rpcenv, $authuser, $new, $existing) = @_;
529 return if !defined($new->{cpu
});
531 my $cpu = PVE
::JSONSchema
::check_format
('pve-vm-cpu-conf', $new->{cpu
});
532 return if !$cpu || !$cpu->{cputype
}; # always allow default
533 my $cputype = $cpu->{cputype
};
535 if ($existing && $existing->{cpu
}) {
536 # changing only other settings doesn't require permissions for CPU model
537 my $existingCpu = PVE
::JSONSchema
::check_format
('pve-vm-cpu-conf', $existing->{cpu
});
538 return if $existingCpu->{cputype
} eq $cputype;
541 if (PVE
::QemuServer
::CPUConfig
::is_custom_model
($cputype)) {
542 $rpcenv->check($authuser, "/nodes", ['Sys.Audit']);
557 my $memoryoptions = {
563 my $hwtypeoptions = {
576 my $generaloptions = {
583 'migrate_downtime' => 1,
584 'migrate_speed' => 1,
596 my $vmpoweroptions = {
603 'vmstatestorage' => 1,
606 my $cloudinitoptions = {
617 my $check_vm_create_serial_perm = sub {
618 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
620 return 1 if $authuser eq 'root@pam';
622 foreach my $opt (keys %{$param}) {
623 next if $opt !~ m/^serial\d+$/;
625 if ($param->{$opt} eq 'socket') {
626 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
628 die "only root can set '$opt' config for real devices\n";
635 my sub check_usb_perm
{
636 my ($rpcenv, $authuser, $vmid, $pool, $opt, $value) = @_;
638 return 1 if $authuser eq 'root@pam';
640 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
642 my $device = PVE
::JSONSchema
::parse_property_string
('pve-qm-usb', $value);
643 if ($device->{host
} && $device->{host
} !~ m/^spice$/i) {
644 die "only root can set '$opt' config for real devices\n";
645 } elsif ($device->{mapping
}) {
646 $rpcenv->check_full($authuser, "/mapping/usb/$device->{mapping}", ['Mapping.Use']);
648 die "either 'host' or 'mapping' must be set.\n";
654 my sub check_vm_create_usb_perm
{
655 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
657 return 1 if $authuser eq 'root@pam';
659 foreach my $opt (keys %{$param}) {
660 next if $opt !~ m/^usb\d+$/;
661 check_usb_perm
($rpcenv, $authuser, $vmid, $pool, $opt, $param->{$opt});
667 my sub check_hostpci_perm
{
668 my ($rpcenv, $authuser, $vmid, $pool, $opt, $value) = @_;
670 return 1 if $authuser eq 'root@pam';
672 my $device = PVE
::JSONSchema
::parse_property_string
('pve-qm-hostpci', $value);
673 if ($device->{host
}) {
674 die "only root can set '$opt' config for non-mapped devices\n";
675 } elsif ($device->{mapping
}) {
676 $rpcenv->check_full($authuser, "/mapping/pci/$device->{mapping}", ['Mapping.Use']);
677 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
679 die "either 'host' or 'mapping' must be set.\n";
685 my sub check_vm_create_hostpci_perm
{
686 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
688 return 1 if $authuser eq 'root@pam';
690 foreach my $opt (keys %{$param}) {
691 next if $opt !~ m/^hostpci\d+$/;
692 check_hostpci_perm
($rpcenv, $authuser, $vmid, $pool, $opt, $param->{$opt});
698 my $check_vm_modify_config_perm = sub {
699 my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
701 return 1 if $authuser eq 'root@pam';
703 foreach my $opt (@$key_list) {
704 # some checks (e.g., disk, serial port, usb) need to be done somewhere
705 # else, as there the permission can be value dependend
706 next if PVE
::QemuServer
::is_valid_drivename
($opt);
707 next if $opt eq 'cdrom';
708 next if $opt =~ m/^(?:unused|serial|usb|hostpci)\d+$/;
709 next if $opt eq 'tags';
712 if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
713 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']);
714 } elsif ($memoryoptions->{$opt}) {
715 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']);
716 } elsif ($hwtypeoptions->{$opt}) {
717 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
718 } elsif ($generaloptions->{$opt}) {
719 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
720 # special case for startup since it changes host behaviour
721 if ($opt eq 'startup') {
722 $rpcenv->check_full($authuser, "/", ['Sys.Modify']);
724 } elsif ($vmpoweroptions->{$opt}) {
725 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']);
726 } elsif ($diskoptions->{$opt}) {
727 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
728 } elsif ($opt =~ m/^net\d+$/) {
729 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
730 } elsif ($cloudinitoptions->{$opt} || $opt =~ m/^ipconfig\d+$/) {
731 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Cloudinit', 'VM.Config.Network'], 1);
732 } elsif ($opt eq 'vmstate') {
733 # the user needs Disk and PowerMgmt privileges to change the vmstate
734 # also needs privileges on the storage, that will be checked later
735 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk', 'VM.PowerMgmt' ]);
737 # catches args, lock, etc.
738 # new options will be checked here
739 die "only root can set '$opt' config\n";
746 sub assert_scsi_feature_compatibility
{
747 my ($opt, $conf, $storecfg, $drive_attributes) = @_;
749 my $drive = PVE
::QemuServer
::Drive
::parse_drive
($opt, $drive_attributes, 1);
751 my $machine_type = PVE
::QemuServer
::get_vm_machine
($conf, undef, $conf->{arch
});
752 my $machine_version = PVE
::QemuServer
::Machine
::extract_version
(
753 $machine_type, PVE
::QemuServer
::kvm_user_version
());
754 my $drivetype = PVE
::QemuServer
::Drive
::get_scsi_devicetype
(
755 $drive, $storecfg, $machine_version);
757 if ($drivetype ne 'hd' && $drivetype ne 'cd') {
758 if ($drive->{product
}) {
760 $opt => "Passing of product information is only supported for 'scsi-hd' and "
761 ."'scsi-cd' devices (e.g. not pass-through).",
764 if ($drive->{vendor
}) {
766 $opt => "Passing of vendor information is only supported for 'scsi-hd' and "
767 ."'scsi-cd' devices (e.g. not pass-through).",
773 __PACKAGE__-
>register_method({
777 description
=> "Virtual machine index (per node).",
779 description
=> "Only list VMs where you have VM.Audit permissons on /vms/<vmid>.",
783 protected
=> 1, # qemu pid files are only readable by root
785 additionalProperties
=> 0,
787 node
=> get_standard_option
('pve-node'),
791 description
=> "Determine the full status of active VMs.",
799 properties
=> $PVE::QemuServer
::vmstatus_return_properties
,
801 links
=> [ { rel
=> 'child', href
=> "{vmid}" } ],
806 my $rpcenv = PVE
::RPCEnvironment
::get
();
807 my $authuser = $rpcenv->get_user();
809 my $vmstatus = PVE
::QemuServer
::vmstatus
(undef, $param->{full
});
812 foreach my $vmid (keys %$vmstatus) {
813 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
815 my $data = $vmstatus->{$vmid};
822 my $parse_restore_archive = sub {
823 my ($storecfg, $archive) = @_;
825 my ($archive_storeid, $archive_volname) = PVE
::Storage
::parse_volume_id
($archive, 1);
829 if (defined($archive_storeid)) {
830 my $scfg = PVE
::Storage
::storage_config
($storecfg, $archive_storeid);
831 $res->{volid
} = $archive;
832 if ($scfg->{type
} eq 'pbs') {
833 $res->{type
} = 'pbs';
837 my $path = PVE
::Storage
::abs_filesystem_path
($storecfg, $archive);
838 $res->{type
} = 'file';
839 $res->{path
} = $path;
843 __PACKAGE__-
>register_method({
847 description
=> "Create or restore a virtual machine.",
849 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
850 "For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
851 "If you create disks you need 'Datastore.AllocateSpace' on any used storage." .
852 "If you use a bridge/vlan, you need 'SDN.Use' on any used bridge/vlan.",
853 user
=> 'all', # check inside
858 additionalProperties
=> 0,
859 properties
=> PVE
::QemuServer
::json_config_properties
(
861 node
=> get_standard_option
('pve-node'),
862 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::Cluster
::complete_next_vmid
}),
864 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.",
868 completion
=> \
&PVE
::QemuServer
::complete_backup_archives
,
870 storage
=> get_standard_option
('pve-storage-id', {
871 description
=> "Default storage.",
873 completion
=> \
&PVE
::QemuServer
::complete_storage
,
878 description
=> "Allow to overwrite existing VM.",
879 requires
=> 'archive',
884 description
=> "Assign a unique random ethernet address.",
885 requires
=> 'archive',
890 description
=> "Start the VM immediately while importing or restoring in the background.",
894 type
=> 'string', format
=> 'pve-poolid',
895 description
=> "Add the VM to the specified pool.",
898 description
=> "Override I/O bandwidth limit (in KiB/s).",
902 default => 'restore limit from datacenter or storage config',
908 description
=> "Start VM after it was created successfully.",
920 my $rpcenv = PVE
::RPCEnvironment
::get
();
921 my $authuser = $rpcenv->get_user();
923 my $node = extract_param
($param, 'node');
924 my $vmid = extract_param
($param, 'vmid');
926 my $archive = extract_param
($param, 'archive');
927 my $is_restore = !!$archive;
929 my $bwlimit = extract_param
($param, 'bwlimit');
930 my $force = extract_param
($param, 'force');
931 my $pool = extract_param
($param, 'pool');
932 my $start_after_create = extract_param
($param, 'start');
933 my $storage = extract_param
($param, 'storage');
934 my $unique = extract_param
($param, 'unique');
935 my $live_restore = extract_param
($param, 'live-restore');
937 if (defined(my $ssh_keys = $param->{sshkeys
})) {
938 $ssh_keys = URI
::Escape
::uri_unescape
($ssh_keys);
939 PVE
::Tools
::validate_ssh_public_keys
($ssh_keys);
942 $param->{cpuunits
} = PVE
::CGroup
::clamp_cpu_shares
($param->{cpuunits
})
943 if defined($param->{cpuunits
}); # clamp value depending on cgroup version
945 PVE
::Cluster
::check_cfs_quorum
();
947 my $filename = PVE
::QemuConfig-
>config_file($vmid);
948 my $storecfg = PVE
::Storage
::config
();
950 if (defined($pool)) {
951 $rpcenv->check_pool_exist($pool);
954 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace'])
955 if defined($storage);
957 if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
959 } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
961 } elsif ($archive && $force && (-f
$filename) &&
962 $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
963 # OK: user has VM.Backup permissions and wants to restore an existing VM
969 for my $opt (sort keys $param->%*) {
970 if (PVE
::QemuServer
::Drive
::is_valid_drivename
($opt)) {
971 raise_param_exc
({ $opt => "option conflicts with option 'archive'" });
975 if ($archive eq '-') {
976 die "pipe requires cli environment\n" if $rpcenv->{type
} ne 'cli';
977 $archive = { type
=> 'pipe' };
979 PVE
::Storage
::check_volume_access
(
988 $archive = $parse_restore_archive->($storecfg, $archive);
992 if (scalar(keys $param->%*) > 0) {
993 &$resolve_cdrom_alias($param);
995 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param, $storage);
997 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
999 &$check_vm_create_serial_perm($rpcenv, $authuser, $vmid, $pool, $param);
1000 check_vm_create_usb_perm
($rpcenv, $authuser, $vmid, $pool, $param);
1001 check_vm_create_hostpci_perm
($rpcenv, $authuser, $vmid, $pool, $param);
1003 PVE
::QemuServer
::check_bridge_access
($rpcenv, $authuser, $param);
1004 &$check_cpu_model_access($rpcenv, $authuser, $param);
1006 $check_drive_param->($param, $storecfg);
1008 PVE
::QemuServer
::add_random_macs
($param);
1011 my $emsg = $is_restore ?
"unable to restore VM $vmid -" : "unable to create VM $vmid -";
1013 eval { PVE
::QemuConfig-
>create_and_lock_config($vmid, $force) };
1014 die "$emsg $@" if $@;
1016 my $restored_data = 0;
1017 my $restorefn = sub {
1018 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1020 PVE
::QemuConfig-
>check_protection($conf, $emsg);
1022 die "$emsg vm is running\n" if PVE
::QemuServer
::check_running
($vmid);
1025 my $restore_options = {
1026 storage
=> $storage,
1029 bwlimit
=> $bwlimit,
1030 live
=> $live_restore,
1031 override_conf
=> $param,
1033 if (my $volid = $archive->{volid
}) {
1034 # best effort, real check is after restoring!
1036 my $old_conf = PVE
::Storage
::extract_vzdump_config
($storecfg, $volid);
1037 PVE
::QemuServer
::restore_merge_config
("backup/qemu-server/$vmid.conf", $old_conf, $param);
1040 warn "Could not extract backed up config: $@\n";
1041 warn "Skipping early checks!\n";
1043 PVE
::QemuServer
::check_restore_permissions
($rpcenv, $authuser, $merged);
1046 if ($archive->{type
} eq 'file' || $archive->{type
} eq 'pipe') {
1047 die "live-restore is only compatible with backup images from a Proxmox Backup Server\n"
1049 PVE
::QemuServer
::restore_file_archive
($archive->{path
} // '-', $vmid, $authuser, $restore_options);
1050 } elsif ($archive->{type
} eq 'pbs') {
1051 PVE
::QemuServer
::restore_proxmox_backup_archive
($archive->{volid
}, $vmid, $authuser, $restore_options);
1053 die "unknown backup archive type\n";
1057 my $restored_conf = PVE
::QemuConfig-
>load_config($vmid);
1058 # Convert restored VM to template if backup was VM template
1059 if (PVE
::QemuConfig-
>is_template($restored_conf)) {
1060 warn "Convert to template.\n";
1061 eval { PVE
::QemuServer
::template_create
($vmid, $restored_conf) };
1065 PVE
::QemuServer
::create_ifaces_ipams_ips
($restored_conf, $vmid) if $unique;
1068 # ensure no old replication state are exists
1069 PVE
::ReplicationState
::delete_guest_states
($vmid);
1071 PVE
::QemuConfig-
>lock_config_full($vmid, 1, $realcmd);
1073 if ($start_after_create && !$live_restore) {
1074 print "Execute autostart\n";
1075 eval { PVE
::API2
::Qemu-
>vm_start({ vmid
=> $vmid, node
=> $node }) };
1080 my $createfn = sub {
1081 my $live_import_mapping = {};
1083 # ensure no old replication state are exists
1084 PVE
::ReplicationState
::delete_guest_states
($vmid);
1088 my $arch = PVE
::QemuServer
::get_vm_arch
($conf);
1090 for my $opt (sort keys $param->%*) {
1091 next if $opt !~ m/^scsi\d+$/;
1092 assert_scsi_feature_compatibility
($opt, $conf, $storecfg, $param->{$opt});
1095 $conf->{meta
} = PVE
::QemuServer
::new_meta_info_string
();
1099 ($vollist, my $created_opts, $live_import_mapping) = create_disks
(
1111 $conf->{$_} = $created_opts->{$_} for keys $created_opts->%*;
1113 if (!$conf->{boot
}) {
1114 my $devs = PVE
::QemuServer
::get_default_bootdevices
($conf);
1115 $conf->{boot
} = PVE
::QemuServer
::print_bootorder
($devs);
1118 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
1119 PVE
::QemuServer
::assert_clipboard_config
($vga);
1121 # auto generate uuid if user did not specify smbios1 option
1122 if (!$conf->{smbios1
}) {
1123 $conf->{smbios1
} = PVE
::QemuServer
::generate_smbios1_uuid
();
1126 if ((!defined($conf->{vmgenid
}) || $conf->{vmgenid
} eq '1') && $arch ne 'aarch64') {
1127 $conf->{vmgenid
} = PVE
::QemuServer
::generate_uuid
();
1130 my $machine = $conf->{machine
};
1131 if (!$machine || $machine =~ m/^(?:pc|q35|virt)$/) {
1132 # always pin Windows' machine version on create, they get to easily confused
1133 if (PVE
::QemuServer
::Helpers
::windows_version
($conf->{ostype
})) {
1134 $conf->{machine
} = PVE
::QemuServer
::windows_get_pinned_machine_version
($machine);
1138 $conf->{lock} = 'import' if $live_import_mapping;
1140 PVE
::QemuConfig-
>write_config($vmid, $conf);
1145 foreach my $volid (@$vollist) {
1146 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
1152 PVE
::AccessControl
::add_vm_to_pool
($vmid, $pool) if $pool;
1154 PVE
::QemuServer
::create_ifaces_ipams_ips
($conf, $vmid);
1157 PVE
::QemuConfig-
>lock_config_full($vmid, 1, $realcmd);
1159 if ($start_after_create && !$live_restore) {
1160 print "Execute autostart\n";
1161 eval { PVE
::API2
::Qemu-
>vm_start({vmid
=> $vmid, node
=> $node}) };
1165 return $live_import_mapping;
1169 my ($code, $worker_name);
1171 $worker_name = 'qmrestore';
1173 eval { $restorefn->() };
1175 eval { PVE
::QemuConfig-
>remove_lock($vmid, 'create') };
1177 if ($restored_data) {
1178 warn "error after data was restored, VM disks should be OK but config may "
1179 ."require adaptions. VM $vmid state is NOT cleaned up.\n";
1181 warn "error before or during data restore, some or all disks were not "
1182 ."completely restored. VM $vmid state is NOT cleaned up.\n";
1188 $worker_name = 'qmcreate';
1190 # If a live import was requested the create function returns
1191 # the mapping for the startup.
1192 my $live_import_mapping = eval { $createfn->() };
1195 my $conffile = PVE
::QemuConfig-
>config_file($vmid);
1196 unlink($conffile) or die "failed to remove config file: $!\n";
1202 if ($live_import_mapping) {
1203 my $import_options = {
1204 bwlimit
=> $bwlimit,
1208 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1209 PVE
::QemuServer
::live_import_from_files
(
1210 $live_import_mapping,
1219 return $rpcenv->fork_worker($worker_name, $vmid, $authuser, $code);
1222 __PACKAGE__-
>register_method({
1227 description
=> "Directory index",
1232 additionalProperties
=> 0,
1234 node
=> get_standard_option
('pve-node'),
1235 vmid
=> get_standard_option
('pve-vmid'),
1243 subdir
=> { type
=> 'string' },
1246 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
1252 { subdir
=> 'config' },
1253 { subdir
=> 'cloudinit' },
1254 { subdir
=> 'pending' },
1255 { subdir
=> 'status' },
1256 { subdir
=> 'unlink' },
1257 { subdir
=> 'vncproxy' },
1258 { subdir
=> 'termproxy' },
1259 { subdir
=> 'migrate' },
1260 { subdir
=> 'resize' },
1261 { subdir
=> 'move' },
1262 { subdir
=> 'rrd' },
1263 { subdir
=> 'rrddata' },
1264 { subdir
=> 'monitor' },
1265 { subdir
=> 'agent' },
1266 { subdir
=> 'snapshot' },
1267 { subdir
=> 'spiceproxy' },
1268 { subdir
=> 'sendkey' },
1269 { subdir
=> 'firewall' },
1270 { subdir
=> 'mtunnel' },
1271 { subdir
=> 'remote_migrate' },
1277 __PACKAGE__-
>register_method ({
1278 subclass
=> "PVE::API2::Firewall::VM",
1279 path
=> '{vmid}/firewall',
1282 __PACKAGE__-
>register_method ({
1283 subclass
=> "PVE::API2::Qemu::Agent",
1284 path
=> '{vmid}/agent',
1287 __PACKAGE__-
>register_method({
1289 path
=> '{vmid}/rrd',
1291 protected
=> 1, # fixme: can we avoid that?
1293 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1295 description
=> "Read VM RRD statistics (returns PNG)",
1297 additionalProperties
=> 0,
1299 node
=> get_standard_option
('pve-node'),
1300 vmid
=> get_standard_option
('pve-vmid'),
1302 description
=> "Specify the time frame you are interested in.",
1304 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
1307 description
=> "The list of datasources you want to display.",
1308 type
=> 'string', format
=> 'pve-configid-list',
1311 description
=> "The RRD consolidation function",
1313 enum
=> [ 'AVERAGE', 'MAX' ],
1321 filename
=> { type
=> 'string' },
1327 return PVE
::RRD
::create_rrd_graph
(
1328 "pve2-vm/$param->{vmid}", $param->{timeframe
},
1329 $param->{ds
}, $param->{cf
});
1333 __PACKAGE__-
>register_method({
1335 path
=> '{vmid}/rrddata',
1337 protected
=> 1, # fixme: can we avoid that?
1339 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1341 description
=> "Read VM RRD statistics",
1343 additionalProperties
=> 0,
1345 node
=> get_standard_option
('pve-node'),
1346 vmid
=> get_standard_option
('pve-vmid'),
1348 description
=> "Specify the time frame you are interested in.",
1350 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
1353 description
=> "The RRD consolidation function",
1355 enum
=> [ 'AVERAGE', 'MAX' ],
1370 return PVE
::RRD
::create_rrd_data
(
1371 "pve2-vm/$param->{vmid}", $param->{timeframe
}, $param->{cf
});
1375 __PACKAGE__-
>register_method({
1376 name
=> 'vm_config',
1377 path
=> '{vmid}/config',
1380 description
=> "Get the virtual machine configuration with pending configuration " .
1381 "changes applied. Set the 'current' parameter to get the current configuration instead.",
1383 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1386 additionalProperties
=> 0,
1388 node
=> get_standard_option
('pve-node'),
1389 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1391 description
=> "Get current values (instead of pending values).",
1396 snapshot
=> get_standard_option
('pve-snapshot-name', {
1397 description
=> "Fetch config values from given snapshot.",
1400 my ($cmd, $pname, $cur, $args) = @_;
1401 PVE
::QemuConfig-
>snapshot_list($args->[0]);
1407 description
=> "The VM configuration.",
1409 properties
=> PVE
::QemuServer
::json_config_properties
({
1412 description
=> 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
1419 raise_param_exc
({ snapshot
=> "cannot use 'snapshot' parameter with 'current'",
1420 current
=> "cannot use 'snapshot' parameter with 'current'"})
1421 if ($param->{snapshot
} && $param->{current
});
1424 if ($param->{snapshot
}) {
1425 $conf = PVE
::QemuConfig-
>load_snapshot_config($param->{vmid
}, $param->{snapshot
});
1427 $conf = PVE
::QemuConfig-
>load_current_config($param->{vmid
}, $param->{current
});
1429 $conf->{cipassword
} = '**********' if $conf->{cipassword
};
1434 __PACKAGE__-
>register_method({
1435 name
=> 'vm_pending',
1436 path
=> '{vmid}/pending',
1439 description
=> "Get the virtual machine configuration with both current and pending values.",
1441 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1444 additionalProperties
=> 0,
1446 node
=> get_standard_option
('pve-node'),
1447 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1456 description
=> "Configuration option name.",
1460 description
=> "Current value.",
1465 description
=> "Pending value.",
1470 description
=> "Indicates a pending delete request if present and not 0. " .
1471 "The value 2 indicates a force-delete request.",
1483 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
1485 my $pending_delete_hash = PVE
::QemuConfig-
>parse_pending_delete($conf->{pending
}->{delete});
1487 $conf->{cipassword
} = '**********' if defined($conf->{cipassword
});
1488 $conf->{pending
}->{cipassword
} = '********** ' if defined($conf->{pending
}->{cipassword
});
1490 return PVE
::GuestHelpers
::config_with_pending_array
($conf, $pending_delete_hash);
1493 __PACKAGE__-
>register_method({
1494 name
=> 'cloudinit_pending',
1495 path
=> '{vmid}/cloudinit',
1498 description
=> "Get the cloudinit configuration with both current and pending values.",
1500 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1503 additionalProperties
=> 0,
1505 node
=> get_standard_option
('pve-node'),
1506 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1515 description
=> "Configuration option name.",
1519 description
=> "Value as it was used to generate the current cloudinit image.",
1524 description
=> "The new pending value.",
1529 description
=> "Indicates a pending delete request if present and not 0. ",
1541 my $vmid = $param->{vmid
};
1542 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1544 my $ci = $conf->{cloudinit
};
1546 $conf->{cipassword
} = '**********' if exists $conf->{cipassword
};
1547 $ci->{cipassword
} = '**********' if exists $ci->{cipassword
};
1551 # All the values that got added
1552 my $added = delete($ci->{added
}) // '';
1553 for my $key (PVE
::Tools
::split_list
($added)) {
1554 push @$res, { key
=> $key, pending
=> $conf->{$key} };
1557 # All already existing values (+ their new value, if it exists)
1558 for my $opt (keys %$cloudinitoptions) {
1559 next if !$conf->{$opt};
1560 next if $added =~ m/$opt/;
1565 if (my $pending = $ci->{$opt}) {
1566 $item->{value
} = $pending;
1567 $item->{pending
} = $conf->{$opt};
1569 $item->{value
} = $conf->{$opt},
1575 # Now, we'll find the deleted ones
1576 for my $opt (keys %$ci) {
1577 next if $conf->{$opt};
1578 push @$res, { key
=> $opt, delete => 1 };
1584 __PACKAGE__-
>register_method({
1585 name
=> 'cloudinit_update',
1586 path
=> '{vmid}/cloudinit',
1590 description
=> "Regenerate and change cloudinit config drive.",
1592 check
=> ['perm', '/vms/{vmid}', ['VM.Config.Cloudinit']],
1595 additionalProperties
=> 0,
1597 node
=> get_standard_option
('pve-node'),
1598 vmid
=> get_standard_option
('pve-vmid'),
1601 returns
=> { type
=> 'null' },
1605 my $rpcenv = PVE
::RPCEnvironment
::get
();
1606 my $authuser = $rpcenv->get_user();
1608 my $vmid = extract_param
($param, 'vmid');
1610 PVE
::QemuConfig-
>lock_config($vmid, sub {
1611 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1612 PVE
::QemuConfig-
>check_lock($conf);
1614 my $storecfg = PVE
::Storage
::config
();
1615 PVE
::QemuServer
::vmconfig_update_cloudinit_drive
($storecfg, $conf, $vmid);
1620 # POST/PUT {vmid}/config implementation
1622 # The original API used PUT (idempotent) an we assumed that all operations
1623 # are fast. But it turned out that almost any configuration change can
1624 # involve hot-plug actions, or disk alloc/free. Such actions can take long
1625 # time to complete and have side effects (not idempotent).
1627 # The new implementation uses POST and forks a worker process. We added
1628 # a new option 'background_delay'. If specified we wait up to
1629 # 'background_delay' second for the worker task to complete. It returns null
1630 # if the task is finished within that time, else we return the UPID.
1632 my $update_vm_api = sub {
1633 my ($param, $sync) = @_;
1635 my $rpcenv = PVE
::RPCEnvironment
::get
();
1637 my $authuser = $rpcenv->get_user();
1639 my $node = extract_param
($param, 'node');
1641 my $vmid = extract_param
($param, 'vmid');
1643 my $digest = extract_param
($param, 'digest');
1645 my $background_delay = extract_param
($param, 'background_delay');
1647 my $skip_cloud_init = extract_param
($param, 'skip_cloud_init');
1649 if (defined(my $cipassword = $param->{cipassword
})) {
1650 # Same logic as in cloud-init (but with the regex fixed...)
1651 $param->{cipassword
} = PVE
::Tools
::encrypt_pw
($cipassword)
1652 if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
1655 my @paramarr = (); # used for log message
1656 foreach my $key (sort keys %$param) {
1657 my $value = $key eq 'cipassword' ?
'<hidden>' : $param->{$key};
1658 push @paramarr, "-$key", $value;
1661 my $skiplock = extract_param
($param, 'skiplock');
1662 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1663 if $skiplock && $authuser ne 'root@pam';
1665 my $delete_str = extract_param
($param, 'delete');
1667 my $revert_str = extract_param
($param, 'revert');
1669 my $force = extract_param
($param, 'force');
1671 if (defined(my $ssh_keys = $param->{sshkeys
})) {
1672 $ssh_keys = URI
::Escape
::uri_unescape
($ssh_keys);
1673 PVE
::Tools
::validate_ssh_public_keys
($ssh_keys);
1676 $param->{cpuunits
} = PVE
::CGroup
::clamp_cpu_shares
($param->{cpuunits
})
1677 if defined($param->{cpuunits
}); # clamp value depending on cgroup version
1679 die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
1681 my $storecfg = PVE
::Storage
::config
();
1683 &$resolve_cdrom_alias($param);
1685 # now try to verify all parameters
1688 foreach my $opt (PVE
::Tools
::split_list
($revert_str)) {
1689 if (!PVE
::QemuServer
::option_exists
($opt)) {
1690 raise_param_exc
({ revert
=> "unknown option '$opt'" });
1693 raise_param_exc
({ delete => "you can't use '-$opt' and " .
1694 "-revert $opt' at the same time" })
1695 if defined($param->{$opt});
1697 $revert->{$opt} = 1;
1701 foreach my $opt (PVE
::Tools
::split_list
($delete_str)) {
1702 $opt = 'ide2' if $opt eq 'cdrom';
1704 raise_param_exc
({ delete => "you can't use '-$opt' and " .
1705 "-delete $opt' at the same time" })
1706 if defined($param->{$opt});
1708 raise_param_exc
({ revert
=> "you can't use '-delete $opt' and " .
1709 "-revert $opt' at the same time" })
1712 if (!PVE
::QemuServer
::option_exists
($opt)) {
1713 raise_param_exc
({ delete => "unknown option '$opt'" });
1719 my $repl_conf = PVE
::ReplicationConfig-
>new();
1720 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
1721 my $check_replication = sub {
1723 return if !$is_replicated;
1724 my $volid = $drive->{file
};
1725 return if !$volid || !($drive->{replicate
}//1);
1726 return if PVE
::QemuServer
::drive_is_cdrom
($drive);
1728 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
1729 die "cannot add non-managed/pass-through volume to a replicated VM\n"
1730 if !defined($storeid);
1732 return if defined($volname) && $volname eq 'cloudinit';
1735 if ($volid =~ $PVE::QemuServer
::Drive
::NEW_DISK_RE
) {
1737 $format = $drive->{format
} || PVE
::Storage
::storage_default_format
($storecfg, $storeid);
1739 $format = (PVE
::Storage
::parse_volname
($storecfg, $volid))[6];
1741 return if PVE
::Storage
::storage_can_replicate
($storecfg, $storeid, $format);
1742 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
1743 return if $scfg->{shared
};
1744 die "cannot add non-replicatable volume to a replicated VM\n";
1747 $check_drive_param->($param, $storecfg, $check_replication);
1749 foreach my $opt (keys %$param) {
1750 if ($opt =~ m/^net(\d+)$/) {
1752 my $net = PVE
::QemuServer
::parse_net
($param->{$opt});
1753 $param->{$opt} = PVE
::QemuServer
::print_net
($net);
1754 } elsif ($opt eq 'vmgenid') {
1755 if ($param->{$opt} eq '1') {
1756 $param->{$opt} = PVE
::QemuServer
::generate_uuid
();
1758 } elsif ($opt eq 'hookscript') {
1759 eval { PVE
::GuestHelpers
::check_hookscript
($param->{$opt}, $storecfg); };
1760 raise_param_exc
({ $opt => $@ }) if $@;
1764 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]);
1766 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
1768 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
1770 PVE
::QemuServer
::check_bridge_access
($rpcenv, $authuser, $param);
1772 my $updatefn = sub {
1774 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1776 die "checksum missmatch (file change by other user?)\n"
1777 if $digest && $digest ne $conf->{digest
};
1779 &$check_cpu_model_access($rpcenv, $authuser, $param, $conf);
1781 # FIXME: 'suspended' lock should probabyl be a state or "weak" lock?!
1782 if (scalar(@delete) && grep { $_ eq 'vmstate'} @delete) {
1783 if (defined($conf->{lock}) && $conf->{lock} eq 'suspended') {
1784 delete $conf->{lock}; # for check lock check, not written out
1785 push @delete, 'lock'; # this is the real deal to write it out
1787 push @delete, 'runningmachine' if $conf->{runningmachine
};
1788 push @delete, 'runningcpu' if $conf->{runningcpu
};
1791 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
1793 foreach my $opt (keys %$revert) {
1794 if (defined($conf->{$opt})) {
1795 $param->{$opt} = $conf->{$opt};
1796 } elsif (defined($conf->{pending
}->{$opt})) {
1801 if ($param->{memory
} || defined($param->{balloon
})) {
1803 my $memory = $param->{memory
} || $conf->{pending
}->{memory
} || $conf->{memory
};
1804 my $maxmem = get_current_memory
($memory);
1805 my $balloon = defined($param->{balloon
}) ?
$param->{balloon
} : $conf->{pending
}->{balloon
} || $conf->{balloon
};
1807 die "balloon value too large (must be smaller than assigned memory)\n"
1808 if $balloon && $balloon > $maxmem;
1811 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: " . join (' ', @paramarr));
1815 print "update VM $vmid: " . join (' ', @paramarr) . "\n";
1817 # write updates to pending section
1819 my $modified = {}; # record what $option we modify
1822 if (my $boot = $conf->{boot
}) {
1823 my $bootcfg = PVE
::JSONSchema
::parse_property_string
('pve-qm-boot', $boot);
1824 @bootorder = PVE
::Tools
::split_list
($bootcfg->{order
}) if $bootcfg && $bootcfg->{order
};
1826 my $bootorder_deleted = grep {$_ eq 'bootorder'} @delete;
1828 my $check_drive_perms = sub {
1829 my ($opt, $val) = @_;
1830 my $drive = PVE
::QemuServer
::parse_drive
($opt, $val, 1);
1831 if (PVE
::QemuServer
::drive_is_cloudinit
($drive)) {
1832 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Cloudinit', 'VM.Config.CDROM']);
1833 } elsif (PVE
::QemuServer
::drive_is_cdrom
($drive, 1)) { # CDROM
1834 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1836 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1841 foreach my $opt (@delete) {
1842 $modified->{$opt} = 1;
1843 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1845 # value of what we want to delete, independent if pending or not
1846 my $val = $conf->{$opt} // $conf->{pending
}->{$opt};
1847 if (!defined($val)) {
1848 warn "cannot delete '$opt' - not set in current configuration!\n";
1849 $modified->{$opt} = 0;
1852 my $is_pending_val = defined($conf->{pending
}->{$opt});
1853 delete $conf->{pending
}->{$opt};
1855 # remove from bootorder if necessary
1856 if (!$bootorder_deleted && @bootorder && grep {$_ eq $opt} @bootorder) {
1857 @bootorder = grep {$_ ne $opt} @bootorder;
1858 $conf->{pending
}->{boot
} = PVE
::QemuServer
::print_bootorder
(\
@bootorder);
1859 $modified->{boot
} = 1;
1862 if ($opt =~ m/^unused/) {
1863 my $drive = PVE
::QemuServer
::parse_drive
($opt, $val);
1864 PVE
::QemuConfig-
>check_protection($conf, "can't remove unused disk '$drive->{file}'");
1865 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1866 if (PVE
::QemuServer
::try_deallocate_drive
($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1867 delete $conf->{$opt};
1868 PVE
::QemuConfig-
>write_config($vmid, $conf);
1870 } elsif ($opt eq 'vmstate') {
1871 PVE
::QemuConfig-
>check_protection($conf, "can't remove vmstate '$val'");
1872 if (PVE
::QemuServer
::try_deallocate_drive
($storecfg, $vmid, $conf, $opt, { file
=> $val }, $rpcenv, $authuser, 1)) {
1873 delete $conf->{$opt};
1874 PVE
::QemuConfig-
>write_config($vmid, $conf);
1876 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
1877 PVE
::QemuConfig-
>check_protection($conf, "can't remove drive '$opt'");
1878 $check_drive_perms->($opt, $val);
1879 PVE
::QemuServer
::vmconfig_register_unused_drive
($storecfg, $vmid, $conf, PVE
::QemuServer
::parse_drive
($opt, $val))
1881 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1882 PVE
::QemuConfig-
>write_config($vmid, $conf);
1883 } elsif ($opt =~ m/^serial\d+$/) {
1884 if ($val eq 'socket') {
1885 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1886 } elsif ($authuser ne 'root@pam') {
1887 die "only root can delete '$opt' config for real devices\n";
1889 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1890 PVE
::QemuConfig-
>write_config($vmid, $conf);
1891 } elsif ($opt =~ m/^usb\d+$/) {
1892 check_usb_perm
($rpcenv, $authuser, $vmid, undef, $opt, $val);
1893 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1894 PVE
::QemuConfig-
>write_config($vmid, $conf);
1895 } elsif ($opt =~ m/^hostpci\d+$/) {
1896 check_hostpci_perm
($rpcenv, $authuser, $vmid, undef, $opt, $val);
1897 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1898 PVE
::QemuConfig-
>write_config($vmid, $conf);
1899 } elsif ($opt eq 'tags') {
1900 assert_tag_permissions
($vmid, $val, '', $rpcenv, $authuser);
1901 delete $conf->{$opt};
1902 PVE
::QemuConfig-
>write_config($vmid, $conf);
1903 } elsif ($opt =~ m/^net\d+$/) {
1904 if ($conf->{$opt}) {
1905 PVE
::QemuServer
::check_bridge_access
(
1908 { $opt => $conf->{$opt} },
1911 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1912 PVE
::QemuConfig-
>write_config($vmid, $conf);
1914 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1915 PVE
::QemuConfig-
>write_config($vmid, $conf);
1919 foreach my $opt (keys %$param) { # add/change
1920 $modified->{$opt} = 1;
1921 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1922 next if defined($conf->{pending
}->{$opt}) && ($param->{$opt} eq $conf->{pending
}->{$opt}); # skip if nothing changed
1924 my $arch = PVE
::QemuServer
::get_vm_arch
($conf);
1926 if (PVE
::QemuServer
::is_valid_drivename
($opt)) {
1928 if ($conf->{$opt}) {
1929 $check_drive_perms->($opt, $conf->{$opt});
1933 $check_drive_perms->($opt, $param->{$opt});
1934 PVE
::QemuServer
::vmconfig_register_unused_drive
($storecfg, $vmid, $conf, PVE
::QemuServer
::parse_drive
($opt, $conf->{pending
}->{$opt}))
1935 if defined($conf->{pending
}->{$opt});
1937 assert_scsi_feature_compatibility
($opt, $conf, $storecfg, $param->{$opt})
1938 if $opt =~ m/^scsi\d+$/;
1940 my (undef, $created_opts) = create_disks
(
1948 {$opt => $param->{$opt}},
1952 $conf->{pending
}->{$_} = $created_opts->{$_} for keys $created_opts->%*;
1954 # default legacy boot order implies all cdroms anyway
1956 # append new CD drives to bootorder to mark them bootable
1957 my $drive = PVE
::QemuServer
::parse_drive
($opt, $param->{$opt}, 1);
1958 if (PVE
::QemuServer
::drive_is_cdrom
($drive, 1) && !grep(/^$opt$/, @bootorder)) {
1959 push @bootorder, $opt;
1960 $conf->{pending
}->{boot
} = PVE
::QemuServer
::print_bootorder
(\
@bootorder);
1961 $modified->{boot
} = 1;
1964 } elsif ($opt =~ m/^serial\d+/) {
1965 if ((!defined($conf->{$opt}) || $conf->{$opt} eq 'socket') && $param->{$opt} eq 'socket') {
1966 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1967 } elsif ($authuser ne 'root@pam') {
1968 die "only root can modify '$opt' config for real devices\n";
1970 $conf->{pending
}->{$opt} = $param->{$opt};
1971 } elsif ($opt eq 'vga') {
1972 my $vga = PVE
::QemuServer
::parse_vga
($param->{$opt});
1973 PVE
::QemuServer
::assert_clipboard_config
($vga);
1974 $conf->{pending
}->{$opt} = $param->{$opt};
1975 } elsif ($opt =~ m/^usb\d+/) {
1976 if (my $olddevice = $conf->{$opt}) {
1977 check_usb_perm
($rpcenv, $authuser, $vmid, undef, $opt, $conf->{$opt});
1979 check_usb_perm
($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
1980 $conf->{pending
}->{$opt} = $param->{$opt};
1981 } elsif ($opt =~ m/^hostpci\d+$/) {
1982 if (my $oldvalue = $conf->{$opt}) {
1983 check_hostpci_perm
($rpcenv, $authuser, $vmid, undef, $opt, $oldvalue);
1985 check_hostpci_perm
($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
1986 $conf->{pending
}->{$opt} = $param->{$opt};
1987 } elsif ($opt eq 'tags') {
1988 assert_tag_permissions
($vmid, $conf->{$opt}, $param->{$opt}, $rpcenv, $authuser);
1989 $conf->{pending
}->{$opt} = PVE
::GuestHelpers
::get_unique_tags
($param->{$opt});
1990 } elsif ($opt =~ m/^net\d+$/) {
1991 if ($conf->{$opt}) {
1992 PVE
::QemuServer
::check_bridge_access
(
1995 { $opt => $conf->{$opt} },
1998 $conf->{pending
}->{$opt} = $param->{$opt};
2000 $conf->{pending
}->{$opt} = $param->{$opt};
2002 if ($opt eq 'boot') {
2003 my $new_bootcfg = PVE
::JSONSchema
::parse_property_string
('pve-qm-boot', $param->{$opt});
2004 if ($new_bootcfg->{order
}) {
2005 my @devs = PVE
::Tools
::split_list
($new_bootcfg->{order
});
2006 for my $dev (@devs) {
2007 my $exists = $conf->{$dev} || $conf->{pending
}->{$dev} || $param->{$dev};
2008 my $deleted = grep {$_ eq $dev} @delete;
2009 die "invalid bootorder: device '$dev' does not exist'\n"
2010 if !$exists || $deleted;
2013 # remove legacy boot order settings if new one set
2014 $conf->{pending
}->{$opt} = PVE
::QemuServer
::print_bootorder
(\
@devs);
2015 PVE
::QemuConfig-
>add_to_pending_delete($conf, "bootdisk")
2016 if $conf->{bootdisk
};
2020 PVE
::QemuConfig-
>remove_from_pending_delete($conf, $opt);
2021 PVE
::QemuConfig-
>write_config($vmid, $conf);
2024 # remove pending changes when nothing changed
2025 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
2026 my $changes = PVE
::QemuConfig-
>cleanup_pending($conf);
2027 PVE
::QemuConfig-
>write_config($vmid, $conf) if $changes;
2029 return if !scalar(keys %{$conf->{pending
}});
2031 my $running = PVE
::QemuServer
::check_running
($vmid);
2033 # apply pending changes
2035 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
2039 PVE
::QemuServer
::vmconfig_hotplug_pending
($vmid, $conf, $storecfg, $modified, $errors);
2041 # cloud_init must be skipped if we are in an incoming, remote live migration
2042 PVE
::QemuServer
::vmconfig_apply_pending
($vmid, $conf, $storecfg, $errors, $skip_cloud_init);
2044 raise_param_exc
($errors) if scalar(keys %$errors);
2053 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
2055 if ($background_delay) {
2057 # Note: It would be better to do that in the Event based HTTPServer
2058 # to avoid blocking call to sleep.
2060 my $end_time = time() + $background_delay;
2062 my $task = PVE
::Tools
::upid_decode
($upid);
2065 while (time() < $end_time) {
2066 $running = PVE
::ProcFSTools
::check_process_running
($task->{pid
}, $task->{pstart
});
2068 sleep(1); # this gets interrupted when child process ends
2072 my $status = PVE
::Tools
::upid_read_status
($upid);
2073 return if !PVE
::Tools
::upid_status_is_error
($status);
2074 die "failed to update VM $vmid: $status\n";
2082 return PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
2085 my $vm_config_perm_list = [
2090 'VM.Config.Network',
2092 'VM.Config.Options',
2093 'VM.Config.Cloudinit',
2096 __PACKAGE__-
>register_method({
2097 name
=> 'update_vm_async',
2098 path
=> '{vmid}/config',
2102 description
=> "Set virtual machine options (asynchrounous API).",
2104 check
=> ['perm', '/vms/{vmid}', $vm_config_perm_list, any
=> 1],
2107 additionalProperties
=> 0,
2108 properties
=> PVE
::QemuServer
::json_config_properties
(
2110 node
=> get_standard_option
('pve-node'),
2111 vmid
=> get_standard_option
('pve-vmid'),
2112 skiplock
=> get_standard_option
('skiplock'),
2114 type
=> 'string', format
=> 'pve-configid-list',
2115 description
=> "A list of settings you want to delete.",
2119 type
=> 'string', format
=> 'pve-configid-list',
2120 description
=> "Revert a pending change.",
2125 description
=> $opt_force_description,
2127 requires
=> 'delete',
2131 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2135 background_delay
=> {
2137 description
=> "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
2143 1, # with_disk_alloc
2150 code
=> $update_vm_api,
2153 __PACKAGE__-
>register_method({
2154 name
=> 'update_vm',
2155 path
=> '{vmid}/config',
2159 description
=> "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
2161 check
=> ['perm', '/vms/{vmid}', $vm_config_perm_list, any
=> 1],
2164 additionalProperties
=> 0,
2165 properties
=> PVE
::QemuServer
::json_config_properties
(
2167 node
=> get_standard_option
('pve-node'),
2168 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2169 skiplock
=> get_standard_option
('skiplock'),
2171 type
=> 'string', format
=> 'pve-configid-list',
2172 description
=> "A list of settings you want to delete.",
2176 type
=> 'string', format
=> 'pve-configid-list',
2177 description
=> "Revert a pending change.",
2182 description
=> $opt_force_description,
2184 requires
=> 'delete',
2188 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2193 1, # with_disk_alloc
2196 returns
=> { type
=> 'null' },
2199 &$update_vm_api($param, 1);
2204 __PACKAGE__-
>register_method({
2205 name
=> 'destroy_vm',
2210 description
=> "Destroy the VM and all used/owned volumes. Removes any VM specific permissions"
2211 ." and firewall rules",
2213 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
2216 additionalProperties
=> 0,
2218 node
=> get_standard_option
('pve-node'),
2219 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
2220 skiplock
=> get_standard_option
('skiplock'),
2223 description
=> "Remove VMID from configurations, like backup & replication jobs and HA.",
2226 'destroy-unreferenced-disks' => {
2228 description
=> "If set, destroy additionally all disks not referenced in the config"
2229 ." but with a matching VMID from all enabled storages.",
2241 my $rpcenv = PVE
::RPCEnvironment
::get
();
2242 my $authuser = $rpcenv->get_user();
2243 my $vmid = $param->{vmid
};
2245 my $skiplock = $param->{skiplock
};
2246 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2247 if $skiplock && $authuser ne 'root@pam';
2249 my $early_checks = sub {
2251 my $conf = PVE
::QemuConfig-
>load_config($vmid);
2252 PVE
::QemuConfig-
>check_protection($conf, "can't remove VM $vmid");
2254 my $ha_managed = PVE
::HA
::Config
::service_is_configured
("vm:$vmid");
2256 if (!$param->{purge
}) {
2257 die "unable to remove VM $vmid - used in HA resources and purge parameter not set.\n"
2259 # don't allow destroy if with replication jobs but no purge param
2260 my $repl_conf = PVE
::ReplicationConfig-
>new();
2261 $repl_conf->check_for_existing_jobs($vmid);
2264 die "VM $vmid is running - destroy failed\n"
2265 if PVE
::QemuServer
::check_running
($vmid);
2275 my $storecfg = PVE
::Storage
::config
();
2277 syslog
('info', "destroy VM $vmid: $upid\n");
2278 PVE
::QemuConfig-
>lock_config($vmid, sub {
2279 # repeat, config might have changed
2280 my $ha_managed = $early_checks->();
2282 my $purge_unreferenced = $param->{'destroy-unreferenced-disks'};
2284 PVE
::QemuServer
::destroy_vm
(
2287 $skiplock, { lock => 'destroyed' },
2288 $purge_unreferenced,
2291 PVE
::AccessControl
::remove_vm_access
($vmid);
2292 PVE
::Firewall
::remove_vmfw_conf
($vmid);
2293 if ($param->{purge
}) {
2294 print "purging VM $vmid from related configurations..\n";
2295 PVE
::ReplicationConfig
::remove_vmid_jobs
($vmid);
2296 PVE
::VZDump
::Plugin
::remove_vmid_from_backup_jobs
($vmid);
2299 PVE
::HA
::Config
::delete_service_from_config
("vm:$vmid");
2300 print "NOTE: removed VM $vmid from HA resource configuration.\n";
2304 # only now remove the zombie config, else we can have reuse race
2305 PVE
::QemuConfig-
>destroy_config($vmid);
2309 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
2312 __PACKAGE__-
>register_method({
2314 path
=> '{vmid}/unlink',
2318 description
=> "Unlink/delete disk images.",
2320 check
=> [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
2323 additionalProperties
=> 0,
2325 node
=> get_standard_option
('pve-node'),
2326 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2328 type
=> 'string', format
=> 'pve-configid-list',
2329 description
=> "A list of disk IDs you want to delete.",
2333 description
=> $opt_force_description,
2338 returns
=> { type
=> 'null'},
2342 $param->{delete} = extract_param
($param, 'idlist');
2344 __PACKAGE__-
>update_vm($param);
2349 # uses good entropy, each char is limited to 6 bit to get printable chars simply
2350 my $gen_rand_chars = sub {
2353 die "invalid length $length" if $length < 1;
2355 my $min = ord('!'); # first printable ascii
2357 my $rand_bytes = Crypt
::OpenSSL
::Random
::random_bytes
($length);
2358 die "failed to generate random bytes!\n"
2361 my $str = join('', map { chr((ord($_) & 0x3F) + $min) } split('', $rand_bytes));
2368 __PACKAGE__-
>register_method({
2370 path
=> '{vmid}/vncproxy',
2374 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2376 description
=> "Creates a TCP VNC proxy connections.",
2378 additionalProperties
=> 0,
2380 node
=> get_standard_option
('pve-node'),
2381 vmid
=> get_standard_option
('pve-vmid'),
2385 description
=> "Prepare for websocket upgrade (only required when using "
2386 ."serial terminal, otherwise upgrade is always possible).",
2388 'generate-password' => {
2392 description
=> "Generates a random password to be used as ticket instead of the API ticket.",
2397 additionalProperties
=> 0,
2399 user
=> { type
=> 'string' },
2400 ticket
=> { type
=> 'string' },
2403 description
=> "Returned if requested with 'generate-password' param."
2404 ." Consists of printable ASCII characters ('!' .. '~').",
2407 cert
=> { type
=> 'string' },
2408 port
=> { type
=> 'integer' },
2409 upid
=> { type
=> 'string' },
2415 my $rpcenv = PVE
::RPCEnvironment
::get
();
2417 my $authuser = $rpcenv->get_user();
2419 my $vmid = $param->{vmid
};
2420 my $node = $param->{node
};
2421 my $websocket = $param->{websocket
};
2423 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # check if VM exists
2427 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2428 $serial = $vga->{type
} if $vga->{type
} =~ m/^serial\d+$/;
2431 my $authpath = "/vms/$vmid";
2433 my $ticket = PVE
::AccessControl
::assemble_vnc_ticket
($authuser, $authpath);
2434 my $password = $ticket;
2435 if ($param->{'generate-password'}) {
2436 $password = $gen_rand_chars->(8);
2439 $sslcert = PVE
::Tools
::file_get_contents
("/etc/pve/pve-root-ca.pem", 8192)
2445 if ($node ne 'localhost' && $node ne PVE
::INotify
::nodename
()) {
2446 (undef, $family) = PVE
::Cluster
::remote_node_ip
($node);
2447 my $sshinfo = PVE
::SSHInfo
::get_ssh_info
($node);
2448 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
2449 $remcmd = PVE
::SSHInfo
::ssh_info_to_command
($sshinfo, defined($serial) ?
'-t' : '-T');
2451 $family = PVE
::Tools
::get_host_address_family
($node);
2454 my $port = PVE
::Tools
::next_vnc_port
($family);
2461 syslog
('info', "starting vnc proxy $upid\n");
2465 if (defined($serial)) {
2467 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $serial, '-escape', '0' ];
2469 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
2470 '-timeout', $timeout, '-authpath', $authpath,
2471 '-perm', 'Sys.Console'];
2473 if ($param->{websocket
}) {
2474 $ENV{PVE_VNC_TICKET
} = $password; # pass ticket to vncterm
2475 push @$cmd, '-notls', '-listen', 'localhost';
2478 push @$cmd, '-c', @$remcmd, @$termcmd;
2480 PVE
::Tools
::run_command
($cmd);
2484 $ENV{LC_PVE_TICKET
} = $password; # set ticket with "qm vncproxy"
2486 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
2488 my $sock = IO
::Socket
::IP-
>new(
2493 GetAddrInfoFlags
=> 0,
2494 ) or die "failed to create socket: $!\n";
2495 # Inside the worker we shouldn't have any previous alarms
2496 # running anyway...:
2498 local $SIG{ALRM
} = sub { die "connection timed out\n" };
2500 accept(my $cli, $sock) or die "connection failed: $!\n";
2503 if (PVE
::Tools
::run_command
($cmd,
2504 output
=> '>&'.fileno($cli),
2505 input
=> '<&'.fileno($cli),
2508 die "Failed to run vncproxy.\n";
2515 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
2517 PVE
::Tools
::wait_for_vnc_port
($port);
2526 $res->{password
} = $password if $param->{'generate-password'};
2531 __PACKAGE__-
>register_method({
2532 name
=> 'termproxy',
2533 path
=> '{vmid}/termproxy',
2537 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2539 description
=> "Creates a TCP proxy connections.",
2541 additionalProperties
=> 0,
2543 node
=> get_standard_option
('pve-node'),
2544 vmid
=> get_standard_option
('pve-vmid'),
2548 enum
=> [qw(serial0 serial1 serial2 serial3)],
2549 description
=> "opens a serial terminal (defaults to display)",
2554 additionalProperties
=> 0,
2556 user
=> { type
=> 'string' },
2557 ticket
=> { type
=> 'string' },
2558 port
=> { type
=> 'integer' },
2559 upid
=> { type
=> 'string' },
2565 my $rpcenv = PVE
::RPCEnvironment
::get
();
2567 my $authuser = $rpcenv->get_user();
2569 my $vmid = $param->{vmid
};
2570 my $node = $param->{node
};
2571 my $serial = $param->{serial
};
2573 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # check if VM exists
2575 if (!defined($serial)) {
2577 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2578 $serial = $vga->{type
} if $vga->{type
} =~ m/^serial\d+$/;
2582 my $authpath = "/vms/$vmid";
2584 my $ticket = PVE
::AccessControl
::assemble_vnc_ticket
($authuser, $authpath);
2589 if ($node ne 'localhost' && $node ne PVE
::INotify
::nodename
()) {
2590 (undef, $family) = PVE
::Cluster
::remote_node_ip
($node);
2591 my $sshinfo = PVE
::SSHInfo
::get_ssh_info
($node);
2592 $remcmd = PVE
::SSHInfo
::ssh_info_to_command
($sshinfo, '-t');
2593 push @$remcmd, '--';
2595 $family = PVE
::Tools
::get_host_address_family
($node);
2598 my $port = PVE
::Tools
::next_vnc_port
($family);
2600 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-escape', '0'];
2601 push @$termcmd, '-iface', $serial if $serial;
2606 syslog
('info', "starting qemu termproxy $upid\n");
2608 my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath,
2609 '--perm', 'VM.Console', '--'];
2610 push @$cmd, @$remcmd, @$termcmd;
2612 PVE
::Tools
::run_command
($cmd);
2615 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
2617 PVE
::Tools
::wait_for_vnc_port
($port);
2627 __PACKAGE__-
>register_method({
2628 name
=> 'vncwebsocket',
2629 path
=> '{vmid}/vncwebsocket',
2632 description
=> "You also need to pass a valid ticket (vncticket).",
2633 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2635 description
=> "Opens a weksocket for VNC traffic.",
2637 additionalProperties
=> 0,
2639 node
=> get_standard_option
('pve-node'),
2640 vmid
=> get_standard_option
('pve-vmid'),
2642 description
=> "Ticket from previous call to vncproxy.",
2647 description
=> "Port number returned by previous vncproxy call.",
2657 port
=> { type
=> 'string' },
2663 my $rpcenv = PVE
::RPCEnvironment
::get
();
2665 my $authuser = $rpcenv->get_user();
2667 my $vmid = $param->{vmid
};
2668 my $node = $param->{node
};
2670 my $authpath = "/vms/$vmid";
2672 PVE
::AccessControl
::verify_vnc_ticket
($param->{vncticket
}, $authuser, $authpath);
2674 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # VM exists ?
2676 # Note: VNC ports are acessible from outside, so we do not gain any
2677 # security if we verify that $param->{port} belongs to VM $vmid. This
2678 # check is done by verifying the VNC ticket (inside VNC protocol).
2680 my $port = $param->{port
};
2682 return { port
=> $port };
2685 __PACKAGE__-
>register_method({
2686 name
=> 'spiceproxy',
2687 path
=> '{vmid}/spiceproxy',
2692 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2694 description
=> "Returns a SPICE configuration to connect to the VM.",
2696 additionalProperties
=> 0,
2698 node
=> get_standard_option
('pve-node'),
2699 vmid
=> get_standard_option
('pve-vmid'),
2700 proxy
=> get_standard_option
('spice-proxy', { optional
=> 1 }),
2703 returns
=> get_standard_option
('remote-viewer-config'),
2707 my $rpcenv = PVE
::RPCEnvironment
::get
();
2709 my $authuser = $rpcenv->get_user();
2711 my $vmid = $param->{vmid
};
2712 my $node = $param->{node
};
2713 my $proxy = $param->{proxy
};
2715 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node);
2716 my $title = "VM $vmid";
2717 $title .= " - ". $conf->{name
} if $conf->{name
};
2719 my $port = PVE
::QemuServer
::spice_port
($vmid);
2721 my ($ticket, undef, $remote_viewer_config) =
2722 PVE
::AccessControl
::remote_viewer_config
($authuser, $vmid, $node, $proxy, $title, $port);
2724 mon_cmd
($vmid, "set_password", protocol
=> 'spice', password
=> $ticket);
2725 mon_cmd
($vmid, "expire_password", protocol
=> 'spice', time => "+30");
2727 return $remote_viewer_config;
2730 __PACKAGE__-
>register_method({
2732 path
=> '{vmid}/status',
2735 description
=> "Directory index",
2740 additionalProperties
=> 0,
2742 node
=> get_standard_option
('pve-node'),
2743 vmid
=> get_standard_option
('pve-vmid'),
2751 subdir
=> { type
=> 'string' },
2754 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
2760 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
2763 { subdir
=> 'current' },
2764 { subdir
=> 'start' },
2765 { subdir
=> 'stop' },
2766 { subdir
=> 'reset' },
2767 { subdir
=> 'shutdown' },
2768 { subdir
=> 'suspend' },
2769 { subdir
=> 'reboot' },
2775 __PACKAGE__-
>register_method({
2776 name
=> 'vm_status',
2777 path
=> '{vmid}/status/current',
2780 protected
=> 1, # qemu pid files are only readable by root
2781 description
=> "Get virtual machine status.",
2783 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2786 additionalProperties
=> 0,
2788 node
=> get_standard_option
('pve-node'),
2789 vmid
=> get_standard_option
('pve-vmid'),
2795 %$PVE::QemuServer
::vmstatus_return_properties
,
2797 description
=> "HA manager service status.",
2801 description
=> "QEMU VGA configuration supports spice.",
2806 description
=> "QEMU Guest Agent is enabled in config.",
2811 description
=> 'Enable a specific clipboard. If not set, depending on'
2812 .' the display type the SPICE one will be added.',
2823 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
2825 my $vmstatus = PVE
::QemuServer
::vmstatus
($param->{vmid
}, 1);
2826 my $status = $vmstatus->{$param->{vmid
}};
2828 $status->{ha
} = PVE
::HA
::Config
::get_service_status
("vm:$param->{vmid}");
2831 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2832 my $spice = defined($vga->{type
}) && $vga->{type
} =~ /^virtio/;
2833 $spice ||= PVE
::QemuServer
::vga_conf_has_spice
($conf->{vga
});
2834 $status->{spice
} = 1 if $spice;
2835 $status->{clipboard
} = $vga->{clipboard
};
2837 $status->{agent
} = 1 if PVE
::QemuServer
::get_qga_key
($conf, 'enabled');
2842 __PACKAGE__-
>register_method({
2844 path
=> '{vmid}/status/start',
2848 description
=> "Start virtual machine.",
2850 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2853 additionalProperties
=> 0,
2855 node
=> get_standard_option
('pve-node'),
2856 vmid
=> get_standard_option
('pve-vmid',
2857 { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
2858 skiplock
=> get_standard_option
('skiplock'),
2859 stateuri
=> get_standard_option
('pve-qm-stateuri'),
2860 migratedfrom
=> get_standard_option
('pve-node',{ optional
=> 1 }),
2863 enum
=> ['secure', 'insecure'],
2864 description
=> "Migration traffic is encrypted using an SSH " .
2865 "tunnel by default. On secure, completely private networks " .
2866 "this can be disabled to increase performance.",
2869 migration_network
=> {
2870 type
=> 'string', format
=> 'CIDR',
2871 description
=> "CIDR of the (sub) network that is used for migration.",
2874 machine
=> get_standard_option
('pve-qemu-machine'),
2876 description
=> "Override QEMU's -cpu argument with the given string.",
2880 targetstorage
=> get_standard_option
('pve-targetstorage'),
2882 description
=> "Wait maximal timeout seconds.",
2885 default => 'max(30, vm memory in GiB)',
2896 my $rpcenv = PVE
::RPCEnvironment
::get
();
2897 my $authuser = $rpcenv->get_user();
2899 my $node = extract_param
($param, 'node');
2900 my $vmid = extract_param
($param, 'vmid');
2901 my $timeout = extract_param
($param, 'timeout');
2902 my $machine = extract_param
($param, 'machine');
2904 my $get_root_param = sub {
2905 my $value = extract_param
($param, $_[0]);
2906 raise_param_exc
({ "$_[0]" => "Only root may use this option." })
2907 if $value && $authuser ne 'root@pam';
2911 my $stateuri = $get_root_param->('stateuri');
2912 my $skiplock = $get_root_param->('skiplock');
2913 my $migratedfrom = $get_root_param->('migratedfrom');
2914 my $migration_type = $get_root_param->('migration_type');
2915 my $migration_network = $get_root_param->('migration_network');
2916 my $targetstorage = $get_root_param->('targetstorage');
2917 my $force_cpu = $get_root_param->('force-cpu');
2921 if ($targetstorage) {
2922 raise_param_exc
({ targetstorage
=> "targetstorage can only by used with migratedfrom." })
2924 $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($targetstorage, 'pve-storage-id') };
2925 raise_param_exc
({ targetstorage
=> "failed to parse storage map: $@" })
2929 # read spice ticket from STDIN
2931 my $nbd_protocol_version = 0;
2932 my $replicated_volumes = {};
2933 my $offline_volumes = {};
2934 if ($stateuri && ($stateuri eq 'tcp' || $stateuri eq 'unix') && $migratedfrom && ($rpcenv->{type
} eq 'cli')) {
2935 while (defined(my $line = <STDIN
>)) {
2937 if ($line =~ m/^spice_ticket: (.+)$/) {
2939 } elsif ($line =~ m/^nbd_protocol_version: (\d+)$/) {
2940 $nbd_protocol_version = $1;
2941 } elsif ($line =~ m/^replicated_volume: (.*)$/) {
2942 $replicated_volumes->{$1} = 1;
2943 } elsif ($line =~ m/^tpmstate0: (.*)$/) { # Deprecated, use offline_volume instead
2944 $offline_volumes->{tpmstate0
} = $1;
2945 } elsif ($line =~ m/^offline_volume: ([^:]+): (.*)$/) {
2946 $offline_volumes->{$1} = $2;
2947 } elsif (!$spice_ticket) {
2948 # fallback for old source node
2949 $spice_ticket = $line;
2951 warn "unknown 'start' parameter on STDIN: '$line'\n";
2956 PVE
::Cluster
::check_cfs_quorum
();
2958 my $storecfg = PVE
::Storage
::config
();
2960 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && !$stateuri && $rpcenv->{type
} ne 'ha') {
2964 print "Requesting HA start for VM $vmid\n";
2966 my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'started'];
2967 PVE
::Tools
::run_command
($cmd);
2971 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
2978 syslog
('info', "start VM $vmid: $upid\n");
2980 my $migrate_opts = {
2981 migratedfrom
=> $migratedfrom,
2982 spice_ticket
=> $spice_ticket,
2983 network
=> $migration_network,
2984 type
=> $migration_type,
2985 storagemap
=> $storagemap,
2986 nbd_proto_version
=> $nbd_protocol_version,
2987 replicated_volumes
=> $replicated_volumes,
2988 offline_volumes
=> $offline_volumes,
2992 statefile
=> $stateuri,
2993 skiplock
=> $skiplock,
2994 forcemachine
=> $machine,
2995 timeout
=> $timeout,
2996 forcecpu
=> $force_cpu,
2999 PVE
::QemuServer
::vm_start
($storecfg, $vmid, $params, $migrate_opts);
3003 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
3007 __PACKAGE__-
>register_method({
3009 path
=> '{vmid}/status/stop',
3013 description
=> "Stop virtual machine. The qemu process will exit immediately. This" .
3014 "is akin to pulling the power plug of a running computer and may damage the VM data",
3016 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3019 additionalProperties
=> 0,
3021 node
=> get_standard_option
('pve-node'),
3022 vmid
=> get_standard_option
('pve-vmid',
3023 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3024 skiplock
=> get_standard_option
('skiplock'),
3025 migratedfrom
=> get_standard_option
('pve-node', { optional
=> 1 }),
3027 description
=> "Wait maximal timeout seconds.",
3033 description
=> "Do not deactivate storage volumes.",
3046 my $rpcenv = PVE
::RPCEnvironment
::get
();
3047 my $authuser = $rpcenv->get_user();
3049 my $node = extract_param
($param, 'node');
3050 my $vmid = extract_param
($param, 'vmid');
3052 my $skiplock = extract_param
($param, 'skiplock');
3053 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3054 if $skiplock && $authuser ne 'root@pam';
3056 my $keepActive = extract_param
($param, 'keepActive');
3057 raise_param_exc
({ keepActive
=> "Only root may use this option." })
3058 if $keepActive && $authuser ne 'root@pam';
3060 my $migratedfrom = extract_param
($param, 'migratedfrom');
3061 raise_param_exc
({ migratedfrom
=> "Only root may use this option." })
3062 if $migratedfrom && $authuser ne 'root@pam';
3065 my $storecfg = PVE
::Storage
::config
();
3067 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && ($rpcenv->{type
} ne 'ha') && !defined($migratedfrom)) {
3072 print "Requesting HA stop for VM $vmid\n";
3074 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", '0'];
3075 PVE
::Tools
::run_command
($cmd);
3079 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
3085 syslog
('info', "stop VM $vmid: $upid\n");
3087 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0,
3088 $param->{timeout
}, 0, 1, $keepActive, $migratedfrom);
3092 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
3096 __PACKAGE__-
>register_method({
3098 path
=> '{vmid}/status/reset',
3102 description
=> "Reset virtual machine.",
3104 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3107 additionalProperties
=> 0,
3109 node
=> get_standard_option
('pve-node'),
3110 vmid
=> get_standard_option
('pve-vmid',
3111 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3112 skiplock
=> get_standard_option
('skiplock'),
3121 my $rpcenv = PVE
::RPCEnvironment
::get
();
3123 my $authuser = $rpcenv->get_user();
3125 my $node = extract_param
($param, 'node');
3127 my $vmid = extract_param
($param, 'vmid');
3129 my $skiplock = extract_param
($param, 'skiplock');
3130 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3131 if $skiplock && $authuser ne 'root@pam';
3133 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3138 PVE
::QemuServer
::vm_reset
($vmid, $skiplock);
3143 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
3146 __PACKAGE__-
>register_method({
3147 name
=> 'vm_shutdown',
3148 path
=> '{vmid}/status/shutdown',
3152 description
=> "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
3153 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
3155 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3158 additionalProperties
=> 0,
3160 node
=> get_standard_option
('pve-node'),
3161 vmid
=> get_standard_option
('pve-vmid',
3162 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3163 skiplock
=> get_standard_option
('skiplock'),
3165 description
=> "Wait maximal timeout seconds.",
3171 description
=> "Make sure the VM stops.",
3177 description
=> "Do not deactivate storage volumes.",
3190 my $rpcenv = PVE
::RPCEnvironment
::get
();
3191 my $authuser = $rpcenv->get_user();
3193 my $node = extract_param
($param, 'node');
3194 my $vmid = extract_param
($param, 'vmid');
3196 my $skiplock = extract_param
($param, 'skiplock');
3197 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3198 if $skiplock && $authuser ne 'root@pam';
3200 my $keepActive = extract_param
($param, 'keepActive');
3201 raise_param_exc
({ keepActive
=> "Only root may use this option." })
3202 if $keepActive && $authuser ne 'root@pam';
3204 my $storecfg = PVE
::Storage
::config
();
3208 # sending a graceful shutdown command to paused VMs runs into timeouts, and even worse, when
3209 # the VM gets resumed later, it still gets the request delivered and powers off
3210 if (PVE
::QemuServer
::vm_is_paused
($vmid, 1)) {
3211 if ($param->{forceStop
}) {
3212 warn "VM is paused - stop instead of shutdown\n";
3215 die "VM is paused - cannot shutdown\n";
3219 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
3221 my $timeout = $param->{timeout
} // 60;
3225 print "Requesting HA stop for VM $vmid\n";
3227 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", "$timeout"];
3228 PVE
::Tools
::run_command
($cmd);
3232 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
3239 syslog
('info', "shutdown VM $vmid: $upid\n");
3241 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0, $param->{timeout
},
3242 $shutdown, $param->{forceStop
}, $keepActive);
3246 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
3250 __PACKAGE__-
>register_method({
3251 name
=> 'vm_reboot',
3252 path
=> '{vmid}/status/reboot',
3256 description
=> "Reboot the VM by shutting it down, and starting it again. Applies pending changes.",
3258 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3261 additionalProperties
=> 0,
3263 node
=> get_standard_option
('pve-node'),
3264 vmid
=> get_standard_option
('pve-vmid',
3265 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3267 description
=> "Wait maximal timeout seconds for the shutdown.",
3280 my $rpcenv = PVE
::RPCEnvironment
::get
();
3281 my $authuser = $rpcenv->get_user();
3283 my $node = extract_param
($param, 'node');
3284 my $vmid = extract_param
($param, 'vmid');
3286 die "VM is paused - cannot shutdown\n" if PVE
::QemuServer
::vm_is_paused
($vmid, 1);
3288 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3293 syslog
('info', "requesting reboot of VM $vmid: $upid\n");
3294 PVE
::QemuServer
::vm_reboot
($vmid, $param->{timeout
});
3298 return $rpcenv->fork_worker('qmreboot', $vmid, $authuser, $realcmd);
3301 __PACKAGE__-
>register_method({
3302 name
=> 'vm_suspend',
3303 path
=> '{vmid}/status/suspend',
3307 description
=> "Suspend virtual machine.",
3309 description
=> "You need 'VM.PowerMgmt' on /vms/{vmid}, and if you have set 'todisk',".
3310 " you need also 'VM.Config.Disk' on /vms/{vmid} and 'Datastore.AllocateSpace'".
3311 " on the storage for the vmstate.",
3312 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3315 additionalProperties
=> 0,
3317 node
=> get_standard_option
('pve-node'),
3318 vmid
=> get_standard_option
('pve-vmid',
3319 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3320 skiplock
=> get_standard_option
('skiplock'),
3325 description
=> 'If set, suspends the VM to disk. Will be resumed on next VM start.',
3327 statestorage
=> get_standard_option
('pve-storage-id', {
3328 description
=> "The storage for the VM state",
3329 requires
=> 'todisk',
3331 completion
=> \
&PVE
::Storage
::complete_storage_enabled
,
3341 my $rpcenv = PVE
::RPCEnvironment
::get
();
3342 my $authuser = $rpcenv->get_user();
3344 my $node = extract_param
($param, 'node');
3345 my $vmid = extract_param
($param, 'vmid');
3347 my $todisk = extract_param
($param, 'todisk') // 0;
3349 my $statestorage = extract_param
($param, 'statestorage');
3351 my $skiplock = extract_param
($param, 'skiplock');
3352 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3353 if $skiplock && $authuser ne 'root@pam';
3355 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3357 die "Cannot suspend HA managed VM to disk\n"
3358 if $todisk && PVE
::HA
::Config
::vm_is_ha_managed
($vmid);
3360 # early check for storage permission, for better user feedback
3362 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
3363 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3365 # cannot save the state of a non-virtualized PCIe device, so resume cannot really work
3366 for my $key (keys %$conf) {
3367 next if $key !~ /^hostpci\d+/;
3368 die "cannot suspend VM to disk due to passed-through PCI device(s), which lack the"
3369 ." possibility to save/restore their internal state\n";
3372 if (!$statestorage) {
3373 # get statestorage from config if none is given
3374 my $storecfg = PVE
::Storage
::config
();
3375 $statestorage = PVE
::QemuServer
::find_vmstate_storage
($conf, $storecfg);
3378 $rpcenv->check($authuser, "/storage/$statestorage", ['Datastore.AllocateSpace']);
3384 syslog
('info', "suspend VM $vmid: $upid\n");
3386 PVE
::QemuServer
::vm_suspend
($vmid, $skiplock, $todisk, $statestorage);
3391 my $taskname = $todisk ?
'qmsuspend' : 'qmpause';
3392 return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd);
3395 __PACKAGE__-
>register_method({
3396 name
=> 'vm_resume',
3397 path
=> '{vmid}/status/resume',
3401 description
=> "Resume virtual machine.",
3403 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3406 additionalProperties
=> 0,
3408 node
=> get_standard_option
('pve-node'),
3409 vmid
=> get_standard_option
('pve-vmid',
3410 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3411 skiplock
=> get_standard_option
('skiplock'),
3412 nocheck
=> { type
=> 'boolean', optional
=> 1 },
3422 my $rpcenv = PVE
::RPCEnvironment
::get
();
3424 my $authuser = $rpcenv->get_user();
3426 my $node = extract_param
($param, 'node');
3428 my $vmid = extract_param
($param, 'vmid');
3430 my $skiplock = extract_param
($param, 'skiplock');
3431 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3432 if $skiplock && $authuser ne 'root@pam';
3434 # nocheck is used as part of migration when config file might be still
3436 my $nocheck = extract_param
($param, 'nocheck');
3437 raise_param_exc
({ nocheck
=> "Only root may use this option." })
3438 if $nocheck && $authuser ne 'root@pam';
3440 my $to_disk_suspended;
3442 PVE
::QemuConfig-
>lock_config($vmid, sub {
3443 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3444 $to_disk_suspended = PVE
::QemuConfig-
>has_lock($conf, 'suspended');
3448 die "VM $vmid not running\n"
3449 if !$to_disk_suspended && !PVE
::QemuServer
::check_running
($vmid, $nocheck);
3454 syslog
('info', "resume VM $vmid: $upid\n");
3456 if (!$to_disk_suspended) {
3457 PVE
::QemuServer
::vm_resume
($vmid, $skiplock, $nocheck);
3459 my $storecfg = PVE
::Storage
::config
();
3460 PVE
::QemuServer
::vm_start
($storecfg, $vmid, { skiplock
=> $skiplock });
3466 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
3469 __PACKAGE__-
>register_method({
3470 name
=> 'vm_sendkey',
3471 path
=> '{vmid}/sendkey',
3475 description
=> "Send key event to virtual machine.",
3477 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
3480 additionalProperties
=> 0,
3482 node
=> get_standard_option
('pve-node'),
3483 vmid
=> get_standard_option
('pve-vmid',
3484 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3485 skiplock
=> get_standard_option
('skiplock'),
3487 description
=> "The key (qemu monitor encoding).",
3492 returns
=> { type
=> 'null'},
3496 my $rpcenv = PVE
::RPCEnvironment
::get
();
3498 my $authuser = $rpcenv->get_user();
3500 my $node = extract_param
($param, 'node');
3502 my $vmid = extract_param
($param, 'vmid');
3504 my $skiplock = extract_param
($param, 'skiplock');
3505 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3506 if $skiplock && $authuser ne 'root@pam';
3508 PVE
::QemuServer
::vm_sendkey
($vmid, $skiplock, $param->{key
});
3513 __PACKAGE__-
>register_method({
3514 name
=> 'vm_feature',
3515 path
=> '{vmid}/feature',
3519 description
=> "Check if feature for virtual machine is available.",
3521 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3524 additionalProperties
=> 0,
3526 node
=> get_standard_option
('pve-node'),
3527 vmid
=> get_standard_option
('pve-vmid'),
3529 description
=> "Feature to check.",
3531 enum
=> [ 'snapshot', 'clone', 'copy' ],
3533 snapname
=> get_standard_option
('pve-snapshot-name', {
3541 hasFeature
=> { type
=> 'boolean' },
3544 items
=> { type
=> 'string' },
3551 my $node = extract_param
($param, 'node');
3553 my $vmid = extract_param
($param, 'vmid');
3555 my $snapname = extract_param
($param, 'snapname');
3557 my $feature = extract_param
($param, 'feature');
3559 my $running = PVE
::QemuServer
::check_running
($vmid);
3561 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3564 my $snap = $conf->{snapshots
}->{$snapname};
3565 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3568 my $storecfg = PVE
::Storage
::config
();
3570 my $nodelist = PVE
::QemuServer
::shared_nodes
($conf, $storecfg);
3571 my $hasFeature = PVE
::QemuConfig-
>has_feature($feature, $conf, $storecfg, $snapname, $running);
3574 hasFeature
=> $hasFeature,
3575 nodes
=> [ keys %$nodelist ],
3579 __PACKAGE__-
>register_method({
3581 path
=> '{vmid}/clone',
3585 description
=> "Create a copy of virtual machine/template.",
3587 description
=> "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
3588 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
3589 "'Datastore.AllocateSpace' on any used storage and 'SDN.Use' on any used bridge/vnet",
3592 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
3594 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
3595 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param
=> 'pool'],
3600 additionalProperties
=> 0,
3602 node
=> get_standard_option
('pve-node'),
3603 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3604 newid
=> get_standard_option
('pve-vmid', {
3605 completion
=> \
&PVE
::Cluster
::complete_next_vmid
,
3606 description
=> 'VMID for the clone.' }),
3609 type
=> 'string', format
=> 'dns-name',
3610 description
=> "Set a name for the new VM.",
3615 description
=> "Description for the new VM.",
3619 type
=> 'string', format
=> 'pve-poolid',
3620 description
=> "Add the new VM to the specified pool.",
3622 snapname
=> get_standard_option
('pve-snapshot-name', {
3625 storage
=> get_standard_option
('pve-storage-id', {
3626 description
=> "Target storage for full clone.",
3630 description
=> "Target format for file storage. Only valid for full clone.",
3633 enum
=> [ 'raw', 'qcow2', 'vmdk'],
3638 description
=> "Create a full copy of all disks. This is always done when " .
3639 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
3641 target
=> get_standard_option
('pve-node', {
3642 description
=> "Target node. Only allowed if the original VM is on shared storage.",
3646 description
=> "Override I/O bandwidth limit (in KiB/s).",
3650 default => 'clone limit from datacenter or storage config',
3660 my $rpcenv = PVE
::RPCEnvironment
::get
();
3661 my $authuser = $rpcenv->get_user();
3663 my $node = extract_param
($param, 'node');
3664 my $vmid = extract_param
($param, 'vmid');
3665 my $newid = extract_param
($param, 'newid');
3666 my $pool = extract_param
($param, 'pool');
3668 my $snapname = extract_param
($param, 'snapname');
3669 my $storage = extract_param
($param, 'storage');
3670 my $format = extract_param
($param, 'format');
3671 my $target = extract_param
($param, 'target');
3673 my $localnode = PVE
::INotify
::nodename
();
3675 if ($target && ($target eq $localnode || $target eq 'localhost')) {
3679 my $running = PVE
::QemuServer
::check_running
($vmid) || 0;
3681 my $load_and_check = sub {
3682 $rpcenv->check_pool_exist($pool) if defined($pool);
3683 PVE
::Cluster
::check_node_exists
($target) if $target;
3685 my $storecfg = PVE
::Storage
::config
();
3688 # check if storage is enabled on local node
3689 PVE
::Storage
::storage_check_enabled
($storecfg, $storage);
3691 # check if storage is available on target node
3692 PVE
::Storage
::storage_check_enabled
($storecfg, $storage, $target);
3693 # clone only works if target storage is shared
3694 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storage);
3695 die "can't clone to non-shared storage '$storage'\n"
3696 if !$scfg->{shared
};
3700 PVE
::Cluster
::check_cfs_quorum
();
3702 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3703 PVE
::QemuConfig-
>check_lock($conf);
3705 my $verify_running = PVE
::QemuServer
::check_running
($vmid) || 0;
3706 die "unexpected state change\n" if $verify_running != $running;
3708 die "snapshot '$snapname' does not exist\n"
3709 if $snapname && !defined( $conf->{snapshots
}->{$snapname});
3711 my $full = $param->{full
} // !PVE
::QemuConfig-
>is_template($conf);
3713 die "parameter 'storage' not allowed for linked clones\n"
3714 if defined($storage) && !$full;
3716 die "parameter 'format' not allowed for linked clones\n"
3717 if defined($format) && !$full;
3719 my $oldconf = $snapname ?
$conf->{snapshots
}->{$snapname} : $conf;
3721 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
3722 PVE
::QemuServer
::check_mapping_access
($rpcenv, $authuser, $oldconf);
3724 PVE
::QemuServer
::check_bridge_access
($rpcenv, $authuser, $oldconf);
3726 die "can't clone VM to node '$target' (VM uses local storage)\n"
3727 if $target && !$sharedvm;
3729 my $conffile = PVE
::QemuConfig-
>config_file($newid);
3730 die "unable to create VM $newid: config file already exists\n"
3733 my $newconf = { lock => 'clone' };
3738 foreach my $opt (keys %$oldconf) {
3739 my $value = $oldconf->{$opt};
3741 # do not copy snapshot related info
3742 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
3743 $opt eq 'vmstate' || $opt eq 'snapstate';
3745 # no need to copy unused images, because VMID(owner) changes anyways
3746 next if $opt =~ m/^unused\d+$/;
3748 die "cannot clone TPM state while VM is running\n"
3749 if $full && $running && !$snapname && $opt eq 'tpmstate0';
3751 # always change MAC! address
3752 if ($opt =~ m/^net(\d+)$/) {
3753 my $net = PVE
::QemuServer
::parse_net
($value);
3754 my $dc = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
3755 $net->{macaddr
} = PVE
::Tools
::random_ether_addr
($dc->{mac_prefix
});
3756 $newconf->{$opt} = PVE
::QemuServer
::print_net
($net);
3757 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
3758 my $drive = PVE
::QemuServer
::parse_drive
($opt, $value);
3759 die "unable to parse drive options for '$opt'\n" if !$drive;
3760 if (PVE
::QemuServer
::drive_is_cdrom
($drive, 1)) {
3761 $newconf->{$opt} = $value; # simply copy configuration
3763 if ($full || PVE
::QemuServer
::drive_is_cloudinit
($drive)) {
3764 die "Full clone feature is not supported for drive '$opt'\n"
3765 if !PVE
::Storage
::volume_has_feature
($storecfg, 'copy', $drive->{file
}, $snapname, $running);
3766 $fullclone->{$opt} = 1;
3768 # not full means clone instead of copy
3769 die "Linked clone feature is not supported for drive '$opt'\n"
3770 if !PVE
::Storage
::volume_has_feature
($storecfg, 'clone', $drive->{file
}, $snapname, $running);
3772 $drives->{$opt} = $drive;
3773 next if PVE
::QemuServer
::drive_is_cloudinit
($drive);
3774 push @$vollist, $drive->{file
};
3777 # copy everything else
3778 $newconf->{$opt} = $value;
3782 return ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone);
3786 my ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone) = $load_and_check->();
3787 my $storecfg = PVE
::Storage
::config
();
3789 # auto generate a new uuid
3790 my $smbios1 = PVE
::QemuServer
::parse_smbios1
($newconf->{smbios1
} || '');
3791 $smbios1->{uuid
} = PVE
::QemuServer
::generate_uuid
();
3792 $newconf->{smbios1
} = PVE
::QemuServer
::print_smbios1
($smbios1);
3793 # auto generate a new vmgenid only if the option was set for template
3794 if ($newconf->{vmgenid
}) {
3795 $newconf->{vmgenid
} = PVE
::QemuServer
::generate_uuid
();
3798 delete $newconf->{template
};
3800 if ($param->{name
}) {
3801 $newconf->{name
} = $param->{name
};
3803 $newconf->{name
} = "Copy-of-VM-" . ($oldconf->{name
} // $vmid);
3806 if ($param->{description
}) {
3807 $newconf->{description
} = $param->{description
};
3810 # create empty/temp config - this fails if VM already exists on other node
3811 # FIXME use PVE::QemuConfig->create_and_lock_config and adapt code
3812 PVE
::Tools
::file_set_contents
($conffile, "# qmclone temporary file\nlock: clone\n");
3814 PVE
::Firewall
::clone_vmfw_conf
($vmid, $newid);
3816 my $newvollist = [];
3823 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
3825 PVE
::Storage
::activate_volumes
($storecfg, $vollist, $snapname);
3827 my $bwlimit = extract_param
($param, 'bwlimit');
3829 my $total_jobs = scalar(keys %{$drives});
3832 foreach my $opt (sort keys %$drives) {
3833 my $drive = $drives->{$opt};
3834 my $skipcomplete = ($total_jobs != $i); # finish after last drive
3835 my $completion = $skipcomplete ?
'skip' : 'complete';
3837 my $src_sid = PVE
::Storage
::parse_volume_id
($drive->{file
});
3838 my $storage_list = [ $src_sid ];
3839 push @$storage_list, $storage if defined($storage);
3840 my $clonelimit = PVE
::Storage
::get_bandwidth_limit
('clone', $storage_list, $bwlimit);
3844 running
=> $running,
3847 snapname
=> $snapname,
3853 storage
=> $storage,
3857 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($oldconf)
3858 if $opt eq 'efidisk0';
3860 my $newdrive = PVE
::QemuServer
::clone_disk
(
3872 $newconf->{$opt} = PVE
::QemuServer
::print_drive
($newdrive);
3874 PVE
::QemuConfig-
>write_config($newid, $newconf);
3878 delete $newconf->{lock};
3880 # do not write pending changes
3881 if (my @changes = keys %{$newconf->{pending
}}) {
3882 my $pending = join(',', @changes);
3883 warn "found pending changes for '$pending', discarding for clone\n";
3884 delete $newconf->{pending
};
3887 PVE
::QemuConfig-
>write_config($newid, $newconf);
3889 PVE
::QemuServer
::create_ifaces_ipams_ips
($newconf, $newid);
3893 # always deactivate volumes – avoids that LVM LVs are active on several nodes
3894 eval { PVE
::Storage
::deactivate_volumes
($storecfg, $vollist, $snapname) };
3895 # but only warn when that fails (e.g., parallel clones keeping them active)
3899 PVE
::Storage
::deactivate_volumes
($storecfg, $newvollist);
3901 my $newconffile = PVE
::QemuConfig-
>config_file($newid, $target);
3902 die "Failed to move config to node '$target' - rename failed: $!\n"
3903 if !rename($conffile, $newconffile);
3906 PVE
::AccessControl
::add_vm_to_pool
($newid, $pool) if $pool;
3909 eval { PVE
::QemuServer
::qemu_blockjobs_cancel
($vmid, $jobs) };
3910 sleep 1; # some storage like rbd need to wait before release volume - really?
3912 foreach my $volid (@$newvollist) {
3913 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
3917 PVE
::Firewall
::remove_vmfw_conf
($newid);
3919 unlink $conffile; # avoid races -> last thing before die
3921 die "clone failed: $err";
3927 # Aquire exclusive lock lock for $newid
3928 my $lock_target_vm = sub {
3929 return PVE
::QemuConfig-
>lock_config_full($newid, 1, $clonefn);
3932 my $lock_source_vm = sub {
3933 # exclusive lock if VM is running - else shared lock is enough;
3935 return PVE
::QemuConfig-
>lock_config_full($vmid, 1, $lock_target_vm);
3937 return PVE
::QemuConfig-
>lock_config_shared($vmid, 1, $lock_target_vm);
3941 $load_and_check->(); # early checks before forking/locking
3943 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $lock_source_vm);
3946 __PACKAGE__-
>register_method({
3947 name
=> 'move_vm_disk',
3948 path
=> '{vmid}/move_disk',
3952 description
=> "Move volume to different storage or to a different VM.",
3954 description
=> "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
3955 "and 'Datastore.AllocateSpace' permissions on the storage. To move ".
3956 "a disk to another VM, you need the permissions on the target VM as well.",
3957 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
3960 additionalProperties
=> 0,
3962 node
=> get_standard_option
('pve-node'),
3963 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3964 'target-vmid' => get_standard_option
('pve-vmid', {
3965 completion
=> \
&PVE
::QemuServer
::complete_vmid
,
3970 description
=> "The disk you want to move.",
3971 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
3973 storage
=> get_standard_option
('pve-storage-id', {
3974 description
=> "Target storage.",
3975 completion
=> \
&PVE
::QemuServer
::complete_storage
,
3980 description
=> "Target Format.",
3981 enum
=> [ 'raw', 'qcow2', 'vmdk' ],
3986 description
=> "Delete the original disk after successful copy. By default the"
3987 ." original disk is kept as unused disk.",
3993 description
=> 'Prevent changes if current configuration file has different SHA1"
3994 ." digest. This can be used to prevent concurrent modifications.',
3999 description
=> "Override I/O bandwidth limit (in KiB/s).",
4003 default => 'move limit from datacenter or storage config',
4007 description
=> "The config key the disk will be moved to on the target VM"
4008 ." (for example, ide0 or scsi1). Default is the source disk key.",
4009 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
4012 'target-digest' => {
4014 description
=> 'Prevent changes if the current config file of the target VM has a"
4015 ." different SHA1 digest. This can be used to detect concurrent modifications.',
4023 description
=> "the task ID.",
4028 my $rpcenv = PVE
::RPCEnvironment
::get
();
4029 my $authuser = $rpcenv->get_user();
4031 my $node = extract_param
($param, 'node');
4032 my $vmid = extract_param
($param, 'vmid');
4033 my $target_vmid = extract_param
($param, 'target-vmid');
4034 my $digest = extract_param
($param, 'digest');
4035 my $target_digest = extract_param
($param, 'target-digest');
4036 my $disk = extract_param
($param, 'disk');
4037 my $target_disk = extract_param
($param, 'target-disk') // $disk;
4038 my $storeid = extract_param
($param, 'storage');
4039 my $format = extract_param
($param, 'format');
4041 my $storecfg = PVE
::Storage
::config
();
4043 my $load_and_check_move = sub {
4044 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4045 PVE
::QemuConfig-
>check_lock($conf);
4047 PVE
::Tools
::assert_if_modified
($digest, $conf->{digest
});
4049 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4051 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
4053 die "disk '$disk' has no associated volume\n" if !$drive->{file
};
4054 die "you can't move a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
4056 my $old_volid = $drive->{file
};
4058 my ($oldstoreid, $oldvolname) = PVE
::Storage
::parse_volume_id
($old_volid);
4059 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
4063 die "you can't move to the same storage with same format\n"
4064 if $oldstoreid eq $storeid && (!$format || !$oldfmt || $oldfmt eq $format);
4066 # this only checks snapshots because $disk is passed!
4067 my $snapshotted = PVE
::QemuServer
::Drive
::is_volume_in_use
(
4073 die "you can't move a disk with snapshots and delete the source\n"
4074 if $snapshotted && $param->{delete};
4076 return ($conf, $drive, $oldstoreid, $snapshotted);
4079 my $move_updatefn = sub {
4080 my ($conf, $drive, $oldstoreid, $snapshotted) = $load_and_check_move->();
4081 my $old_volid = $drive->{file
};
4083 PVE
::Cluster
::log_msg
(
4086 "move disk VM $vmid: move --disk $disk --storage $storeid"
4089 my $running = PVE
::QemuServer
::check_running
($vmid);
4091 PVE
::Storage
::activate_volumes
($storecfg, [ $drive->{file
} ]);
4093 my $newvollist = [];
4099 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
4101 warn "moving disk with snapshots, snapshots will not be moved!\n"
4104 my $bwlimit = extract_param
($param, 'bwlimit');
4105 my $movelimit = PVE
::Storage
::get_bandwidth_limit
(
4107 [$oldstoreid, $storeid],
4113 running
=> $running,
4122 storage
=> $storeid,
4126 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($conf)
4127 if $disk eq 'efidisk0';
4129 my $newdrive = PVE
::QemuServer
::clone_disk
(
4140 $conf->{$disk} = PVE
::QemuServer
::print_drive
($newdrive);
4142 PVE
::QemuConfig-
>add_unused_volume($conf, $old_volid) if !$param->{delete};
4144 # convert moved disk to base if part of template
4145 PVE
::QemuServer
::template_create
($vmid, $conf, $disk)
4146 if PVE
::QemuConfig-
>is_template($conf);
4148 PVE
::QemuConfig-
>write_config($vmid, $conf);
4150 my $do_trim = PVE
::QemuServer
::get_qga_key
($conf, 'fstrim_cloned_disks');
4151 if ($running && $do_trim && PVE
::QemuServer
::qga_check_running
($vmid)) {
4152 eval { mon_cmd
($vmid, "guest-fstrim") };
4156 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
4157 PVE
::Storage
::deactivate_volumes
($storecfg, [ $newdrive->{file
} ])
4163 foreach my $volid (@$newvollist) {
4164 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid) };
4167 die "storage migration failed: $err";
4170 if ($param->{delete}) {
4172 PVE
::Storage
::deactivate_volumes
($storecfg, [$old_volid]);
4173 PVE
::Storage
::vdisk_free
($storecfg, $old_volid);
4179 my $load_and_check_reassign_configs = sub {
4180 my $vmlist = PVE
::Cluster
::get_vmlist
()->{ids
};
4182 die "could not find VM ${vmid}\n" if !exists($vmlist->{$vmid});
4183 die "could not find target VM ${target_vmid}\n" if !exists($vmlist->{$target_vmid});
4185 my $source_node = $vmlist->{$vmid}->{node
};
4186 my $target_node = $vmlist->{$target_vmid}->{node
};
4188 die "Both VMs need to be on the same node ($source_node != $target_node)\n"
4189 if $source_node ne $target_node;
4191 my $source_conf = PVE
::QemuConfig-
>load_config($vmid);
4192 PVE
::QemuConfig-
>check_lock($source_conf);
4193 my $target_conf = PVE
::QemuConfig-
>load_config($target_vmid);
4194 PVE
::QemuConfig-
>check_lock($target_conf);
4196 die "Can't move disks from or to template VMs\n"
4197 if ($source_conf->{template
} || $target_conf->{template
});
4200 eval { PVE
::Tools
::assert_if_modified
($digest, $source_conf->{digest
}) };
4201 die "VM ${vmid}: $@" if $@;
4204 if ($target_digest) {
4205 eval { PVE
::Tools
::assert_if_modified
($target_digest, $target_conf->{digest
}) };
4206 die "VM ${target_vmid}: $@" if $@;
4209 die "Disk '${disk}' for VM '$vmid' does not exist\n" if !defined($source_conf->{$disk});
4211 die "Target disk key '${target_disk}' is already in use for VM '$target_vmid'\n"
4212 if $target_conf->{$target_disk};
4214 my $drive = PVE
::QemuServer
::parse_drive
(
4216 $source_conf->{$disk},
4218 die "failed to parse source disk - $@\n" if !$drive;
4220 my $source_volid = $drive->{file
};
4222 die "disk '${disk}' has no associated volume\n" if !$source_volid;
4223 die "CD drive contents can't be moved to another VM\n"
4224 if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
4226 my $storeid = PVE
::Storage
::parse_volume_id
($source_volid, 1);
4227 die "Volume '$source_volid' not managed by PVE\n" if !defined($storeid);
4229 die "Can't move disk used by a snapshot to another VM\n"
4230 if PVE
::QemuServer
::Drive
::is_volume_in_use
($storecfg, $source_conf, $disk, $source_volid);
4231 die "Storage does not support moving of this disk to another VM\n"
4232 if (!PVE
::Storage
::volume_has_feature
($storecfg, 'rename', $source_volid));
4233 die "Cannot move disk to another VM while the source VM is running - detach first\n"
4234 if PVE
::QemuServer
::check_running
($vmid) && $disk !~ m/^unused\d+$/;
4236 # now re-parse using target disk slot format
4237 if ($target_disk =~ /^unused\d+$/) {
4238 $drive = PVE
::QemuServer
::parse_drive
(
4243 $drive = PVE
::QemuServer
::parse_drive
(
4245 $source_conf->{$disk},
4248 die "failed to parse source disk for target disk format - $@\n" if !$drive;
4250 my $repl_conf = PVE
::ReplicationConfig-
>new();
4251 if ($repl_conf->check_for_existing_jobs($target_vmid, 1)) {
4252 my $format = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4253 die "Cannot move disk to a replicated VM. Storage does not support replication!\n"
4254 if !PVE
::Storage
::storage_can_replicate
($storecfg, $storeid, $format);
4257 return ($source_conf, $target_conf, $drive);
4262 print STDERR
"$msg\n";
4265 my $disk_reassignfn = sub {
4266 return PVE
::QemuConfig-
>lock_config($vmid, sub {
4267 return PVE
::QemuConfig-
>lock_config($target_vmid, sub {
4268 my ($source_conf, $target_conf, $drive) = &$load_and_check_reassign_configs();
4270 my $source_volid = $drive->{file
};
4272 print "moving disk '$disk' from VM '$vmid' to '$target_vmid'\n";
4273 my ($storeid, $source_volname) = PVE
::Storage
::parse_volume_id
($source_volid);
4275 my $fmt = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4277 my $new_volid = PVE
::Storage
::rename_volume
(
4283 $drive->{file
} = $new_volid;
4285 my $boot_order = PVE
::QemuServer
::device_bootorder
($source_conf);
4286 if (defined(delete $boot_order->{$disk})) {
4287 print "removing disk '$disk' from boot order config\n";
4288 my $boot_devs = [ sort { $boot_order->{$a} <=> $boot_order->{$b} } keys %$boot_order ];
4289 $source_conf->{boot
} = PVE
::QemuServer
::print_bootorder
($boot_devs);
4292 delete $source_conf->{$disk};
4293 print "removing disk '${disk}' from VM '${vmid}' config\n";
4294 PVE
::QemuConfig-
>write_config($vmid, $source_conf);
4296 my $drive_string = PVE
::QemuServer
::print_drive
($drive);
4298 if ($target_disk =~ /^unused\d+$/) {
4299 $target_conf->{$target_disk} = $drive_string;
4300 PVE
::QemuConfig-
>write_config($target_vmid, $target_conf);
4305 vmid
=> $target_vmid,
4306 digest
=> $target_digest,
4307 $target_disk => $drive_string,
4313 # remove possible replication snapshots
4314 if (PVE
::Storage
::volume_has_feature
(
4320 PVE
::Replication
::prepare
(
4330 print "Failed to remove replication snapshots on moved disk " .
4331 "'$target_disk'. Manual cleanup could be necessary.\n";
4338 if ($target_vmid && $storeid) {
4339 my $msg = "either set 'storage' or 'target-vmid', but not both";
4340 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4341 } elsif ($target_vmid) {
4342 $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
4343 if $authuser ne 'root@pam';
4345 raise_param_exc
({ 'target-vmid' => "must be different than source VMID to reassign disk" })
4346 if $vmid eq $target_vmid;
4348 my (undef, undef, $drive) = &$load_and_check_reassign_configs();
4349 my $storage = PVE
::Storage
::parse_volume_id
($drive->{file
});
4350 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
4352 return $rpcenv->fork_worker(
4354 "${vmid}-${disk}>${target_vmid}-${target_disk}",
4358 } elsif ($storeid) {
4359 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4361 die "cannot move disk '$disk', only configured disks can be moved to another storage\n"
4362 if $disk =~ m/^unused\d+$/;
4364 $load_and_check_move->(); # early checks before forking/locking
4367 PVE
::QemuConfig-
>lock_config($vmid, $move_updatefn);
4370 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
4372 my $msg = "both 'storage' and 'target-vmid' missing, either needs to be set";
4373 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4377 my $check_vm_disks_local = sub {
4378 my ($storecfg, $vmconf, $vmid) = @_;
4380 my $local_disks = {};
4382 # add some more information to the disks e.g. cdrom
4383 PVE
::QemuServer
::foreach_volid
($vmconf, sub {
4384 my ($volid, $attr) = @_;
4386 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
4388 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
4389 return if $scfg->{shared
};
4391 # The shared attr here is just a special case where the vdisk
4392 # is marked as shared manually
4393 return if $attr->{shared
};
4394 return if $attr->{cdrom
} and $volid eq "none";
4396 if (exists $local_disks->{$volid}) {
4397 @{$local_disks->{$volid}}{keys %$attr} = values %$attr
4399 $local_disks->{$volid} = $attr;
4400 # ensure volid is present in case it's needed
4401 $local_disks->{$volid}->{volid
} = $volid;
4405 return $local_disks;
4408 __PACKAGE__-
>register_method({
4409 name
=> 'migrate_vm_precondition',
4410 path
=> '{vmid}/migrate',
4414 description
=> "Get preconditions for migration.",
4416 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4419 additionalProperties
=> 0,
4421 node
=> get_standard_option
('pve-node'),
4422 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4423 target
=> get_standard_option
('pve-node', {
4424 description
=> "Target node.",
4425 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4433 running
=> { type
=> 'boolean' },
4437 description
=> "List nodes allowed for offline migration, only passed if VM is offline"
4439 not_allowed_nodes
=> {
4442 description
=> "List not allowed nodes with additional informations, only passed if VM is offline"
4446 description
=> "List local disks including CD-Rom, unsused and not referenced disks"
4448 local_resources
=> {
4450 description
=> "List local resources e.g. pci, usb"
4452 'mapped-resources' => {
4454 description
=> "List of mapped resources e.g. pci, usb"
4461 my $rpcenv = PVE
::RPCEnvironment
::get
();
4463 my $authuser = $rpcenv->get_user();
4465 PVE
::Cluster
::check_cfs_quorum
();
4469 my $vmid = extract_param
($param, 'vmid');
4470 my $target = extract_param
($param, 'target');
4471 my $localnode = PVE
::INotify
::nodename
();
4475 my $vmconf = PVE
::QemuConfig-
>load_config($vmid);
4476 my $storecfg = PVE
::Storage
::config
();
4479 # try to detect errors early
4480 PVE
::QemuConfig-
>check_lock($vmconf);
4482 $res->{running
} = PVE
::QemuServer
::check_running
($vmid) ?
1:0;
4484 my ($local_resources, $mapped_resources, $missing_mappings_by_node) =
4485 PVE
::QemuServer
::check_local_resources
($vmconf, 1);
4486 delete $missing_mappings_by_node->{$localnode};
4488 my $vga = PVE
::QemuServer
::parse_vga
($vmconf->{vga
});
4489 if ($res->{running
} && $vga->{'clipboard'} && $vga->{'clipboard'} eq 'vnc') {
4490 push $local_resources->@*, "clipboard=vnc";
4493 # if vm is not running, return target nodes where local storage/mapped devices are available
4494 # for offline migration
4495 if (!$res->{running
}) {
4496 $res->{allowed_nodes
} = [];
4497 my $checked_nodes = PVE
::QemuServer
::check_local_storage_availability
($vmconf, $storecfg);
4498 delete $checked_nodes->{$localnode};
4500 foreach my $node (keys %$checked_nodes) {
4501 my $missing_mappings = $missing_mappings_by_node->{$node};
4502 if (scalar($missing_mappings->@*)) {
4503 $checked_nodes->{$node}->{'unavailable-resources'} = $missing_mappings;
4507 if (!defined($checked_nodes->{$node}->{unavailable_storages
})) {
4508 push @{$res->{allowed_nodes
}}, $node;
4512 $res->{not_allowed_nodes
} = $checked_nodes;
4515 my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
4516 $res->{local_disks
} = [ values %$local_disks ];;
4518 $res->{local_resources
} = $local_resources;
4519 $res->{'mapped-resources'} = $mapped_resources;
4526 __PACKAGE__-
>register_method({
4527 name
=> 'migrate_vm',
4528 path
=> '{vmid}/migrate',
4532 description
=> "Migrate virtual machine. Creates a new migration task.",
4534 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4537 additionalProperties
=> 0,
4539 node
=> get_standard_option
('pve-node'),
4540 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4541 target
=> get_standard_option
('pve-node', {
4542 description
=> "Target node.",
4543 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4547 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4552 description
=> "Allow to migrate VMs which use local devices. Only root may use this option.",
4557 enum
=> ['secure', 'insecure'],
4558 description
=> "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
4561 migration_network
=> {
4562 type
=> 'string', format
=> 'CIDR',
4563 description
=> "CIDR of the (sub) network that is used for migration.",
4566 "with-local-disks" => {
4568 description
=> "Enable live storage migration for local disk",
4571 targetstorage
=> get_standard_option
('pve-targetstorage', {
4572 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4575 description
=> "Override I/O bandwidth limit (in KiB/s).",
4579 default => 'migrate limit from datacenter or storage config',
4585 description
=> "the task ID.",
4590 my $rpcenv = PVE
::RPCEnvironment
::get
();
4591 my $authuser = $rpcenv->get_user();
4593 my $target = extract_param
($param, 'target');
4595 my $localnode = PVE
::INotify
::nodename
();
4596 raise_param_exc
({ target
=> "target is local node."}) if $target eq $localnode;
4598 PVE
::Cluster
::check_cfs_quorum
();
4600 PVE
::Cluster
::check_node_exists
($target);
4602 my $targetip = PVE
::Cluster
::remote_node_ip
($target);
4604 my $vmid = extract_param
($param, 'vmid');
4606 raise_param_exc
({ force
=> "Only root may use this option." })
4607 if $param->{force
} && $authuser ne 'root@pam';
4609 raise_param_exc
({ migration_type
=> "Only root may use this option." })
4610 if $param->{migration_type
} && $authuser ne 'root@pam';
4612 # allow root only until better network permissions are available
4613 raise_param_exc
({ migration_network
=> "Only root may use this option." })
4614 if $param->{migration_network
} && $authuser ne 'root@pam';
4617 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4619 # try to detect errors early
4621 PVE
::QemuConfig-
>check_lock($conf);
4623 if (PVE
::QemuServer
::check_running
($vmid)) {
4624 die "can't migrate running VM without --online\n" if !$param->{online
};
4626 my $repl_conf = PVE
::ReplicationConfig-
>new();
4627 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
4628 my $is_replicated_to_target = defined($repl_conf->find_local_replication_job($vmid, $target));
4629 if (!$param->{force
} && $is_replicated && !$is_replicated_to_target) {
4630 die "Cannot live-migrate replicated VM to node '$target' - not a replication " .
4631 "target. Use 'force' to override.\n";
4634 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4635 $param->{online
} = 0;
4638 my $storecfg = PVE
::Storage
::config
();
4639 if (my $targetstorage = $param->{targetstorage
}) {
4640 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($targetstorage, 'pve-storage-id') };
4641 raise_param_exc
({ targetstorage
=> "failed to parse storage map: $@" })
4644 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk'])
4645 if !defined($storagemap->{identity
});
4647 foreach my $target_sid (values %{$storagemap->{entries
}}) {
4648 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $target_sid, $target);
4651 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storagemap->{default}, $target)
4652 if $storagemap->{default};
4654 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target)
4655 if $storagemap->{identity
};
4657 $param->{storagemap
} = $storagemap;
4659 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target);
4662 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
4667 print "Requesting HA migration for VM $vmid to node $target\n";
4669 my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
4670 PVE
::Tools
::run_command
($cmd);
4674 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
4679 PVE
::QemuMigrate-
>migrate($target, $targetip, $vmid, $param);
4683 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
4686 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
4691 __PACKAGE__-
>register_method({
4692 name
=> 'remote_migrate_vm',
4693 path
=> '{vmid}/remote_migrate',
4697 description
=> "Migrate virtual machine to a remote cluster. Creates a new migration task. EXPERIMENTAL feature!",
4699 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4702 additionalProperties
=> 0,
4704 node
=> get_standard_option
('pve-node'),
4705 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4706 'target-vmid' => get_standard_option
('pve-vmid', { optional
=> 1 }),
4707 'target-endpoint' => get_standard_option
('proxmox-remote', {
4708 description
=> "Remote target endpoint",
4712 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4717 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.",
4721 'target-storage' => get_standard_option
('pve-targetstorage', {
4722 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4725 'target-bridge' => {
4727 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.",
4728 format
=> 'bridge-pair-list',
4731 description
=> "Override I/O bandwidth limit (in KiB/s).",
4735 default => 'migrate limit from datacenter or storage config',
4741 description
=> "the task ID.",
4746 my $rpcenv = PVE
::RPCEnvironment
::get
();
4747 my $authuser = $rpcenv->get_user();
4749 my $source_vmid = extract_param
($param, 'vmid');
4750 my $target_endpoint = extract_param
($param, 'target-endpoint');
4751 my $target_vmid = extract_param
($param, 'target-vmid') // $source_vmid;
4753 my $delete = extract_param
($param, 'delete') // 0;
4755 PVE
::Cluster
::check_cfs_quorum
();
4758 my $conf = PVE
::QemuConfig-
>load_config($source_vmid);
4760 PVE
::QemuConfig-
>check_lock($conf);
4762 raise_param_exc
({ vmid
=> "cannot migrate HA-managed VM to remote cluster" })
4763 if PVE
::HA
::Config
::vm_is_ha_managed
($source_vmid);
4765 my $remote = PVE
::JSONSchema
::parse_property_string
('proxmox-remote', $target_endpoint);
4767 # TODO: move this as helper somewhere appropriate?
4769 protocol
=> 'https',
4770 host
=> $remote->{host
},
4771 port
=> $remote->{port
} // 8006,
4772 apitoken
=> $remote->{apitoken
},
4776 if ($fp = $remote->{fingerprint
}) {
4777 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 };
4780 print "Establishing API connection with remote at '$remote->{host}'\n";
4782 my $api_client = PVE
::APIClient
::LWP-
>new(%$conn_args);
4784 if (!defined($fp)) {
4785 my $cert_info = $api_client->get("/nodes/localhost/certificates/info");
4786 foreach my $cert (@$cert_info) {
4787 my $filename = $cert->{filename
};
4788 next if $filename ne 'pveproxy-ssl.pem' && $filename ne 'pve-ssl.pem';
4789 $fp = $cert->{fingerprint
} if !$fp || $filename eq 'pveproxy-ssl.pem';
4791 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 }
4795 my $repl_conf = PVE
::ReplicationConfig-
>new();
4796 my $is_replicated = $repl_conf->check_for_existing_jobs($source_vmid, 1);
4797 die "cannot remote-migrate replicated VM\n" if $is_replicated;
4799 if (PVE
::QemuServer
::check_running
($source_vmid)) {
4800 die "can't migrate running VM without --online\n" if !$param->{online
};
4803 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4804 $param->{online
} = 0;
4807 my $storecfg = PVE
::Storage
::config
();
4808 my $target_storage = extract_param
($param, 'target-storage');
4809 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($target_storage, 'pve-storage-id') };
4810 raise_param_exc
({ 'target-storage' => "failed to parse storage map: $@" })
4813 my $target_bridge = extract_param
($param, 'target-bridge');
4814 my $bridgemap = eval { PVE
::JSONSchema
::parse_idmap
($target_bridge, 'pve-bridge-id') };
4815 raise_param_exc
({ 'target-bridge' => "failed to parse bridge map: $@" })
4818 die "remote migration requires explicit storage mapping!\n"
4819 if $storagemap->{identity
};
4821 $param->{storagemap
} = $storagemap;
4822 $param->{bridgemap
} = $bridgemap;
4823 $param->{remote
} = {
4824 conn
=> $conn_args, # re-use fingerprint for tunnel
4825 client
=> $api_client,
4826 vmid
=> $target_vmid,
4828 $param->{migration_type
} = 'websocket';
4829 $param->{'with-local-disks'} = 1;
4830 $param->{delete} = $delete if $delete;
4832 my $cluster_status = $api_client->get("/cluster/status");
4834 foreach my $entry (@$cluster_status) {
4835 next if $entry->{type
} ne 'node';
4836 if ($entry->{local}) {
4837 $target_node = $entry->{name
};
4842 die "couldn't determine endpoint's node name\n"
4843 if !defined($target_node);
4846 PVE
::QemuMigrate-
>migrate($target_node, $remote->{host
}, $source_vmid, $param);
4850 return PVE
::GuestHelpers
::guest_migration_lock
($source_vmid, 10, $realcmd);
4853 return $rpcenv->fork_worker('qmigrate', $source_vmid, $authuser, $worker);
4856 __PACKAGE__-
>register_method({
4858 path
=> '{vmid}/monitor',
4862 description
=> "Execute QEMU monitor commands.",
4864 description
=> "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
4865 check
=> ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
4868 additionalProperties
=> 0,
4870 node
=> get_standard_option
('pve-node'),
4871 vmid
=> get_standard_option
('pve-vmid'),
4874 description
=> "The monitor command.",
4878 returns
=> { type
=> 'string'},
4882 my $rpcenv = PVE
::RPCEnvironment
::get
();
4883 my $authuser = $rpcenv->get_user();
4886 my $command = shift;
4887 return $command =~ m/^\s*info(\s+|$)/
4888 || $command =~ m/^\s*help\s*$/;
4891 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
4892 if !&$is_ro($param->{command
});
4894 my $vmid = $param->{vmid
};
4896 my $conf = PVE
::QemuConfig-
>load_config ($vmid); # check if VM exists
4900 $res = PVE
::QemuServer
::Monitor
::hmp_cmd
($vmid, $param->{command
});
4902 $res = "ERROR: $@" if $@;
4907 __PACKAGE__-
>register_method({
4908 name
=> 'resize_vm',
4909 path
=> '{vmid}/resize',
4913 description
=> "Extend volume size.",
4915 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
4918 additionalProperties
=> 0,
4920 node
=> get_standard_option
('pve-node'),
4921 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4922 skiplock
=> get_standard_option
('skiplock'),
4925 description
=> "The disk you want to resize.",
4926 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
4930 pattern
=> '\+?\d+(\.\d+)?[KMGT]?',
4931 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.",
4935 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
4943 description
=> "the task ID.",
4948 my $rpcenv = PVE
::RPCEnvironment
::get
();
4950 my $authuser = $rpcenv->get_user();
4952 my $node = extract_param
($param, 'node');
4954 my $vmid = extract_param
($param, 'vmid');
4956 my $digest = extract_param
($param, 'digest');
4958 my $disk = extract_param
($param, 'disk');
4960 my $sizestr = extract_param
($param, 'size');
4962 my $skiplock = extract_param
($param, 'skiplock');
4963 raise_param_exc
({ skiplock
=> "Only root may use this option." })
4964 if $skiplock && $authuser ne 'root@pam';
4966 my $storecfg = PVE
::Storage
::config
();
4968 my $updatefn = sub {
4970 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4972 die "checksum missmatch (file change by other user?)\n"
4973 if $digest && $digest ne $conf->{digest
};
4974 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
4976 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4978 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
4980 my (undef, undef, undef, undef, undef, undef, $format) =
4981 PVE
::Storage
::parse_volname
($storecfg, $drive->{file
});
4983 my $volid = $drive->{file
};
4985 die "disk '$disk' has no associated volume\n" if !$volid;
4987 die "you can't resize a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive);
4989 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
4991 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4993 PVE
::Storage
::activate_volumes
($storecfg, [$volid]);
4994 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid, 5);
4996 die "Could not determine current size of volume '$volid'\n" if !defined($size);
4998 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
4999 my ($ext, $newsize, $unit) = ($1, $2, $4);
5002 $newsize = $newsize * 1024;
5003 } elsif ($unit eq 'M') {
5004 $newsize = $newsize * 1024 * 1024;
5005 } elsif ($unit eq 'G') {
5006 $newsize = $newsize * 1024 * 1024 * 1024;
5007 } elsif ($unit eq 'T') {
5008 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
5011 $newsize += $size if $ext;
5012 $newsize = int($newsize);
5014 die "shrinking disks is not supported\n" if $newsize < $size;
5016 return if $size == $newsize;
5018 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
5020 PVE
::QemuServer
::qemu_block_resize
($vmid, "drive-$disk", $storecfg, $volid, $newsize);
5022 $drive->{size
} = $newsize;
5023 $conf->{$disk} = PVE
::QemuServer
::print_drive
($drive);
5025 PVE
::QemuConfig-
>write_config($vmid, $conf);
5029 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
5032 return $rpcenv->fork_worker('resize', $vmid, $authuser, $worker);
5035 __PACKAGE__-
>register_method({
5036 name
=> 'snapshot_list',
5037 path
=> '{vmid}/snapshot',
5039 description
=> "List all snapshots.",
5041 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5044 protected
=> 1, # qemu pid files are only readable by root
5046 additionalProperties
=> 0,
5048 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5049 node
=> get_standard_option
('pve-node'),
5058 description
=> "Snapshot identifier. Value 'current' identifies the current VM.",
5062 description
=> "Snapshot includes RAM.",
5067 description
=> "Snapshot description.",
5071 description
=> "Snapshot creation time",
5073 renderer
=> 'timestamp',
5077 description
=> "Parent snapshot identifier.",
5083 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
5088 my $vmid = $param->{vmid
};
5090 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5091 my $snaphash = $conf->{snapshots
} || {};
5095 foreach my $name (keys %$snaphash) {
5096 my $d = $snaphash->{$name};
5099 snaptime
=> $d->{snaptime
} || 0,
5100 vmstate
=> $d->{vmstate
} ?
1 : 0,
5101 description
=> $d->{description
} || '',
5103 $item->{parent
} = $d->{parent
} if $d->{parent
};
5104 $item->{snapstate
} = $d->{snapstate
} if $d->{snapstate
};
5108 my $running = PVE
::QemuServer
::check_running
($vmid, 1) ?
1 : 0;
5111 digest
=> $conf->{digest
},
5112 running
=> $running,
5113 description
=> "You are here!",
5115 $current->{parent
} = $conf->{parent
} if $conf->{parent
};
5117 push @$res, $current;
5122 __PACKAGE__-
>register_method({
5124 path
=> '{vmid}/snapshot',
5128 description
=> "Snapshot a VM.",
5130 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5133 additionalProperties
=> 0,
5135 node
=> get_standard_option
('pve-node'),
5136 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5137 snapname
=> get_standard_option
('pve-snapshot-name'),
5141 description
=> "Save the vmstate",
5146 description
=> "A textual description or comment.",
5152 description
=> "the task ID.",
5157 my $rpcenv = PVE
::RPCEnvironment
::get
();
5159 my $authuser = $rpcenv->get_user();
5161 my $node = extract_param
($param, 'node');
5163 my $vmid = extract_param
($param, 'vmid');
5165 my $snapname = extract_param
($param, 'snapname');
5167 die "unable to use snapshot name 'current' (reserved name)\n"
5168 if $snapname eq 'current';
5170 die "unable to use snapshot name 'pending' (reserved name)\n"
5171 if lc($snapname) eq 'pending';
5174 PVE
::Cluster
::log_msg
('info', $authuser, "snapshot VM $vmid: $snapname");
5175 PVE
::QemuConfig-
>snapshot_create($vmid, $snapname, $param->{vmstate
},
5176 $param->{description
});
5179 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
5182 __PACKAGE__-
>register_method({
5183 name
=> 'snapshot_cmd_idx',
5184 path
=> '{vmid}/snapshot/{snapname}',
5191 additionalProperties
=> 0,
5193 vmid
=> get_standard_option
('pve-vmid'),
5194 node
=> get_standard_option
('pve-node'),
5195 snapname
=> get_standard_option
('pve-snapshot-name'),
5204 links
=> [ { rel
=> 'child', href
=> "{cmd}" } ],
5211 push @$res, { cmd
=> 'rollback' };
5212 push @$res, { cmd
=> 'config' };
5217 __PACKAGE__-
>register_method({
5218 name
=> 'update_snapshot_config',
5219 path
=> '{vmid}/snapshot/{snapname}/config',
5223 description
=> "Update snapshot metadata.",
5225 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5228 additionalProperties
=> 0,
5230 node
=> get_standard_option
('pve-node'),
5231 vmid
=> get_standard_option
('pve-vmid'),
5232 snapname
=> get_standard_option
('pve-snapshot-name'),
5236 description
=> "A textual description or comment.",
5240 returns
=> { type
=> 'null' },
5244 my $rpcenv = PVE
::RPCEnvironment
::get
();
5246 my $authuser = $rpcenv->get_user();
5248 my $vmid = extract_param
($param, 'vmid');
5250 my $snapname = extract_param
($param, 'snapname');
5252 return if !defined($param->{description
});
5254 my $updatefn = sub {
5256 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5258 PVE
::QemuConfig-
>check_lock($conf);
5260 my $snap = $conf->{snapshots
}->{$snapname};
5262 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5264 $snap->{description
} = $param->{description
} if defined($param->{description
});
5266 PVE
::QemuConfig-
>write_config($vmid, $conf);
5269 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
5274 __PACKAGE__-
>register_method({
5275 name
=> 'get_snapshot_config',
5276 path
=> '{vmid}/snapshot/{snapname}/config',
5279 description
=> "Get snapshot configuration",
5281 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback', 'VM.Audit' ], any
=> 1],
5284 additionalProperties
=> 0,
5286 node
=> get_standard_option
('pve-node'),
5287 vmid
=> get_standard_option
('pve-vmid'),
5288 snapname
=> get_standard_option
('pve-snapshot-name'),
5291 returns
=> { type
=> "object" },
5295 my $rpcenv = PVE
::RPCEnvironment
::get
();
5297 my $authuser = $rpcenv->get_user();
5299 my $vmid = extract_param
($param, 'vmid');
5301 my $snapname = extract_param
($param, 'snapname');
5303 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5305 my $snap = $conf->{snapshots
}->{$snapname};
5307 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5312 __PACKAGE__-
>register_method({
5314 path
=> '{vmid}/snapshot/{snapname}/rollback',
5318 description
=> "Rollback VM state to specified snapshot.",
5320 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any
=> 1],
5323 additionalProperties
=> 0,
5325 node
=> get_standard_option
('pve-node'),
5326 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5327 snapname
=> get_standard_option
('pve-snapshot-name'),
5330 description
=> "Whether the VM should get started after rolling back successfully."
5331 . " (Note: VMs will be automatically started if the snapshot includes RAM.)",
5339 description
=> "the task ID.",
5344 my $rpcenv = PVE
::RPCEnvironment
::get
();
5346 my $authuser = $rpcenv->get_user();
5348 my $node = extract_param
($param, 'node');
5350 my $vmid = extract_param
($param, 'vmid');
5352 my $snapname = extract_param
($param, 'snapname');
5355 PVE
::Cluster
::log_msg
('info', $authuser, "rollback snapshot VM $vmid: $snapname");
5356 PVE
::QemuConfig-
>snapshot_rollback($vmid, $snapname);
5358 if ($param->{start
} && !PVE
::QemuServer
::Helpers
::vm_running_locally
($vmid)) {
5359 PVE
::API2
::Qemu-
>vm_start({ vmid
=> $vmid, node
=> $node });
5364 # hold migration lock, this makes sure that nobody create replication snapshots
5365 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
5368 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
5371 __PACKAGE__-
>register_method({
5372 name
=> 'delsnapshot',
5373 path
=> '{vmid}/snapshot/{snapname}',
5377 description
=> "Delete a VM snapshot.",
5379 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5382 additionalProperties
=> 0,
5384 node
=> get_standard_option
('pve-node'),
5385 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5386 snapname
=> get_standard_option
('pve-snapshot-name'),
5390 description
=> "For removal from config file, even if removing disk snapshots fails.",
5396 description
=> "the task ID.",
5401 my $rpcenv = PVE
::RPCEnvironment
::get
();
5403 my $authuser = $rpcenv->get_user();
5405 my $node = extract_param
($param, 'node');
5407 my $vmid = extract_param
($param, 'vmid');
5409 my $snapname = extract_param
($param, 'snapname');
5412 my $do_delete = sub {
5414 PVE
::Cluster
::log_msg
('info', $authuser, "delete snapshot VM $vmid: $snapname");
5415 PVE
::QemuConfig-
>snapshot_delete($vmid, $snapname, $param->{force
});
5419 if ($param->{force
}) {
5422 eval { PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $do_delete); };
5424 die $err if $lock_obtained;
5425 die "Failed to obtain guest migration lock - replication running?\n";
5430 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
5433 __PACKAGE__-
>register_method({
5435 path
=> '{vmid}/template',
5439 description
=> "Create a Template.",
5441 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid}",
5442 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
5445 additionalProperties
=> 0,
5447 node
=> get_standard_option
('pve-node'),
5448 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
5452 description
=> "If you want to convert only 1 disk to base image.",
5453 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
5460 description
=> "the task ID.",
5465 my $rpcenv = PVE
::RPCEnvironment
::get
();
5467 my $authuser = $rpcenv->get_user();
5469 my $node = extract_param
($param, 'node');
5471 my $vmid = extract_param
($param, 'vmid');
5473 my $disk = extract_param
($param, 'disk');
5475 my $load_and_check = sub {
5476 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5478 PVE
::QemuConfig-
>check_lock($conf);
5480 die "unable to create template, because VM contains snapshots\n"
5481 if $conf->{snapshots
} && scalar(keys %{$conf->{snapshots
}});
5483 die "you can't convert a template to a template\n"
5484 if PVE
::QemuConfig-
>is_template($conf) && !$disk;
5486 die "you can't convert a VM to template if VM is running\n"
5487 if PVE
::QemuServer
::check_running
($vmid);
5492 $load_and_check->();
5495 PVE
::QemuConfig-
>lock_config($vmid, sub {
5496 my $conf = $load_and_check->();
5498 $conf->{template
} = 1;
5499 PVE
::QemuConfig-
>write_config($vmid, $conf);
5501 PVE
::QemuServer
::template_create
($vmid, $conf, $disk);
5505 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
5508 __PACKAGE__-
>register_method({
5509 name
=> 'cloudinit_generated_config_dump',
5510 path
=> '{vmid}/cloudinit/dump',
5513 description
=> "Get automatically generated cloudinit config.",
5515 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5518 additionalProperties
=> 0,
5520 node
=> get_standard_option
('pve-node'),
5521 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5523 description
=> 'Config type.',
5525 enum
=> ['user', 'network', 'meta'],
5535 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
5537 return PVE
::QemuServer
::Cloudinit
::dump_cloudinit_config
($conf, $param->{vmid
}, $param->{type
});
5540 __PACKAGE__-
>register_method({
5542 path
=> '{vmid}/mtunnel',
5545 description
=> 'Migration tunnel endpoint - only for internal use by VM migration.',
5549 ['perm', '/vms/{vmid}', [ 'VM.Allocate' ]],
5550 ['perm', '/', [ 'Sys.Incoming' ]],
5552 description
=> "You need 'VM.Allocate' permissions on '/vms/{vmid}' and Sys.Incoming" .
5553 " on '/'. Further permission checks happen during the actual migration.",
5556 additionalProperties
=> 0,
5558 node
=> get_standard_option
('pve-node'),
5559 vmid
=> get_standard_option
('pve-vmid'),
5562 format
=> 'pve-storage-id-list',
5564 description
=> 'List of storages to check permission and availability. Will be checked again for all actually used storages during migration.',
5568 format
=> 'pve-bridge-id-list',
5570 description
=> 'List of network bridges to check availability. Will be checked again for actually used bridges during migration.',
5575 additionalProperties
=> 0,
5577 upid
=> { type
=> 'string' },
5578 ticket
=> { type
=> 'string' },
5579 socket => { type
=> 'string' },
5585 my $rpcenv = PVE
::RPCEnvironment
::get
();
5586 my $authuser = $rpcenv->get_user();
5588 my $node = extract_param
($param, 'node');
5589 my $vmid = extract_param
($param, 'vmid');
5591 my $storages = extract_param
($param, 'storages');
5592 my $bridges = extract_param
($param, 'bridges');
5594 my $nodename = PVE
::INotify
::nodename
();
5596 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
5597 if $node ne 'localhost' && $node ne $nodename;
5601 my $storecfg = PVE
::Storage
::config
();
5602 foreach my $storeid (PVE
::Tools
::split_list
($storages)) {
5603 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storeid, $node);
5606 foreach my $bridge (PVE
::Tools
::split_list
($bridges)) {
5607 PVE
::Network
::read_bridge_mtu
($bridge);
5610 PVE
::Cluster
::check_cfs_quorum
();
5612 my $lock = 'create';
5613 eval { PVE
::QemuConfig-
>create_and_lock_config($vmid, 0, $lock); };
5615 raise_param_exc
({ vmid
=> "unable to create empty VM config - $@"})
5620 storecfg
=> PVE
::Storage
::config
(),
5625 my $run_locked = sub {
5626 my ($code, $params) = @_;
5627 return PVE
::QemuConfig-
>lock_config($state->{vmid
}, sub {
5628 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5630 $state->{conf
} = $conf;
5632 die "Encountered wrong lock - aborting mtunnel command handling.\n"
5633 if $state->{lock} && !PVE
::QemuConfig-
>has_lock($conf, $state->{lock});
5635 return $code->($params);
5643 description
=> 'Full VM config, adapted for target cluster/node',
5645 'firewall-config' => {
5647 description
=> 'VM firewall config',
5652 format
=> PVE
::JSONSchema
::get_standard_option
('pve-qm-image-format'),
5655 format
=> 'pve-storage-id',
5659 description
=> 'parsed drive information without volid and format',
5665 description
=> 'params passed to vm_start_nolock',
5669 description
=> 'migrate_opts passed to vm_start_nolock',
5675 description
=> 'socket path for which the ticket should be valid. must be known to current mtunnel instance.',
5681 description
=> 'remove VM config and disks, aborting migration',
5685 'disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'disk-import'},
5686 'query-disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'query-disk-import'},
5687 bwlimit
=> $PVE::StorageTunnel
::cmd_schema-
>{bwlimit
},
5690 my $cmd_handlers = {
5692 # compared against other end's version
5693 # bump/reset for breaking changes
5694 # bump/bump for opt-in changes
5696 api
=> $PVE::QemuMigrate
::WS_TUNNEL_VERSION
,
5703 # parse and write out VM FW config if given
5704 if (my $fw_conf = $params->{'firewall-config'}) {
5705 my ($path, $fh) = PVE
::Tools
::tempfile_contents
($fw_conf, 700);
5712 ipset_comments
=> {},
5714 my $cluster_fw_conf = PVE
::Firewall
::load_clusterfw_conf
();
5716 # TODO: add flag for strict parsing?
5717 # TODO: add import sub that does all this given raw content?
5718 my $vmfw_conf = PVE
::Firewall
::generic_fw_config_parser
($path, $cluster_fw_conf, $empty_conf, 'vm');
5719 $vmfw_conf->{vmid
} = $state->{vmid
};
5720 PVE
::Firewall
::save_vmfw_conf
($state->{vmid
}, $vmfw_conf);
5722 $state->{cleanup
}->{fw
} = 1;
5725 my $conf_fn = "incoming/qemu-server/$state->{vmid}.conf";
5726 my $new_conf = PVE
::QemuServer
::parse_vm_config
($conf_fn, $params->{conf
}, 1);
5727 delete $new_conf->{lock};
5728 delete $new_conf->{digest
};
5730 # TODO handle properly?
5731 delete $new_conf->{snapshots
};
5732 delete $new_conf->{parent
};
5733 delete $new_conf->{pending
};
5735 # not handled by update_vm_api
5736 my $vmgenid = delete $new_conf->{vmgenid
};
5737 my $meta = delete $new_conf->{meta
};
5738 my $cloudinit = delete $new_conf->{cloudinit
}; # this is informational only
5739 $new_conf->{skip_cloud_init
} = 1; # re-use image from source side
5741 $new_conf->{vmid
} = $state->{vmid
};
5742 $new_conf->{node
} = $node;
5744 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, 'create');
5747 $update_vm_api->($new_conf, 1);
5750 # revert to locked previous config
5751 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5752 $conf->{lock} = 'create';
5753 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5758 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5759 $conf->{lock} = 'migrate';
5760 $conf->{vmgenid
} = $vmgenid if defined($vmgenid);
5761 $conf->{meta
} = $meta if defined($meta);
5762 $conf->{cloudinit
} = $cloudinit if defined($cloudinit);
5763 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5765 $state->{lock} = 'migrate';
5771 return PVE
::StorageTunnel
::handle_bwlimit
($params);
5776 my $format = $params->{format
};
5777 my $storeid = $params->{storage
};
5778 my $drive = $params->{drive
};
5780 $check_storage_access_migrate->($rpcenv, $authuser, $state->{storecfg
}, $storeid, $node);
5783 default => $storeid,
5786 my $source_volumes = {
5796 my $res = PVE
::QemuServer
::vm_migrate_alloc_nbd_disks
($state->{storecfg
}, $state->{vmid
}, $source_volumes, $storagemap);
5797 if (defined($res->{disk
})) {
5798 $state->{cleanup
}->{volumes
}->{$res->{disk
}->{volid
}} = 1;
5799 return $res->{disk
};
5801 die "failed to allocate NBD disk..\n";
5804 'disk-import' => sub {
5807 $check_storage_access_migrate->(
5815 $params->{unix
} = "/run/qemu-server/$state->{vmid}.storage";
5817 return PVE
::StorageTunnel
::handle_disk_import
($state, $params);
5819 'query-disk-import' => sub {
5822 return PVE
::StorageTunnel
::handle_query_disk_import
($state, $params);
5827 my $info = PVE
::QemuServer
::vm_start_nolock
(
5831 $params->{start_params
},
5832 $params->{migrate_opts
},
5836 if ($info->{migrate
}->{proto
} ne 'unix') {
5837 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5838 die "migration over non-UNIX sockets not possible\n";
5841 my $socket = $info->{migrate
}->{addr
};
5842 chown $state->{socket_uid
}, -1, $socket;
5843 $state->{sockets
}->{$socket} = 1;
5845 my $unix_sockets = $info->{migrate
}->{unix_sockets
};
5846 foreach my $socket (@$unix_sockets) {
5847 chown $state->{socket_uid
}, -1, $socket;
5848 $state->{sockets
}->{$socket} = 1;
5853 if (PVE
::QemuServer
::qga_check_running
($state->{vmid
})) {
5854 eval { mon_cmd
($state->{vmid
}, "guest-fstrim") };
5855 warn "fstrim failed: $@\n" if $@;
5860 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5864 PVE
::QemuServer
::nbd_stop
($state->{vmid
});
5868 if (PVE
::QemuServer
::Helpers
::vm_running_locally
($state->{vmid
})) {
5869 PVE
::QemuServer
::vm_resume
($state->{vmid
}, 1, 1);
5871 die "VM $state->{vmid} not running\n";
5876 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, $state->{lock});
5877 delete $state->{lock};
5883 my $path = $params->{path
};
5885 die "Not allowed to generate ticket for unknown socket '$path'\n"
5886 if !defined($state->{sockets
}->{$path});
5888 return { ticket
=> PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$path") };
5893 if ($params->{cleanup
}) {
5894 if ($state->{cleanup
}->{fw
}) {
5895 PVE
::Firewall
::remove_vmfw_conf
($state->{vmid
});
5898 for my $volid (keys $state->{cleanup
}->{volumes
}->%*) {
5899 print "freeing volume '$volid' as part of cleanup\n";
5900 eval { PVE
::Storage
::vdisk_free
($state->{storecfg
}, $volid) };
5904 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5907 print "switching to exit-mode, waiting for client to disconnect\n";
5914 my $socket_addr = "/run/qemu-server/$state->{vmid}.mtunnel";
5915 unlink $socket_addr;
5917 $state->{socket} = IO
::Socket
::UNIX-
>new(
5918 Type
=> SOCK_STREAM
(),
5919 Local
=> $socket_addr,
5923 $state->{socket_uid
} = getpwnam('www-data')
5924 or die "Failed to resolve user 'www-data' to numeric UID\n";
5925 chown $state->{socket_uid
}, -1, $socket_addr;
5928 print "mtunnel started\n";
5930 my $conn = eval { PVE
::Tools
::run_with_timeout
(300, sub { $state->{socket}->accept() }) };
5932 warn "Failed to accept tunnel connection - $@\n";
5934 warn "Removing tunnel socket..\n";
5935 unlink $state->{socket};
5937 warn "Removing temporary VM config..\n";
5939 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5942 die "Exiting mtunnel\n";
5945 $state->{conn
} = $conn;
5947 my $reply_err = sub {
5950 my $reply = JSON
::encode_json
({
5951 success
=> JSON
::false
,
5954 $conn->print("$reply\n");
5958 my $reply_ok = sub {
5961 $res->{success
} = JSON
::true
;
5962 my $reply = JSON
::encode_json
($res);
5963 $conn->print("$reply\n");
5967 while (my $line = <$conn>) {
5970 # untaint, we validate below if needed
5971 ($line) = $line =~ /^(.*)$/;
5972 my $parsed = eval { JSON
::decode_json
($line) };
5974 $reply_err->("failed to parse command - $@");
5978 my $cmd = delete $parsed->{cmd
};
5979 if (!defined($cmd)) {
5980 $reply_err->("'cmd' missing");
5981 } elsif ($state->{exit}) {
5982 $reply_err->("tunnel is in exit-mode, processing '$cmd' cmd not possible");
5984 } elsif (my $handler = $cmd_handlers->{$cmd}) {
5985 print "received command '$cmd'\n";
5987 if ($cmd_desc->{$cmd}) {
5988 PVE
::JSONSchema
::validate
($parsed, $cmd_desc->{$cmd});
5992 my $res = $run_locked->($handler, $parsed);
5995 $reply_err->("failed to handle '$cmd' command - $@")
5998 $reply_err->("unknown command '$cmd' given");
6002 if ($state->{exit}) {
6003 print "mtunnel exited\n";
6005 die "mtunnel exited unexpectedly\n";
6009 my $socket_addr = "/run/qemu-server/$vmid.mtunnel";
6010 my $ticket = PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$socket_addr");
6011 my $upid = $rpcenv->fork_worker('qmtunnel', $vmid, $authuser, $realcmd);
6016 socket => $socket_addr,
6020 __PACKAGE__-
>register_method({
6021 name
=> 'mtunnelwebsocket',
6022 path
=> '{vmid}/mtunnelwebsocket',
6025 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.",
6026 user
=> 'all', # check inside
6028 description
=> 'Migration tunnel endpoint for websocket upgrade - only for internal use by VM migration.',
6030 additionalProperties
=> 0,
6032 node
=> get_standard_option
('pve-node'),
6033 vmid
=> get_standard_option
('pve-vmid'),
6036 description
=> "unix socket to forward to",
6040 description
=> "ticket return by initial 'mtunnel' API call, or retrieved via 'ticket' tunnel command",
6047 port
=> { type
=> 'string', optional
=> 1 },
6048 socket => { type
=> 'string', optional
=> 1 },
6054 my $rpcenv = PVE
::RPCEnvironment
::get
();
6055 my $authuser = $rpcenv->get_user();
6057 my $nodename = PVE
::INotify
::nodename
();
6058 my $node = extract_param
($param, 'node');
6060 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
6061 if $node ne 'localhost' && $node ne $nodename;
6063 my $vmid = $param->{vmid
};
6065 PVE
::QemuConfig-
>load_config($vmid);
6067 my $socket = $param->{socket};
6068 PVE
::AccessControl
::verify_tunnel_ticket
($param->{ticket
}, $authuser, "/socket/$socket");
6070 return { socket => $socket };