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_device_type
(
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 = PVE
::QemuServer
::Machine
::parse_machine
($conf->{machine
});
1131 my $machine = $machine_conf->{type
};
1132 if (!$machine || $machine =~ m/^(?:pc|q35|virt)$/) {
1133 # always pin Windows' machine version on create, they get to easily confused
1134 if (PVE
::QemuServer
::Helpers
::windows_version
($conf->{ostype
})) {
1135 $machine_conf->{type
} = PVE
::QemuServer
::windows_get_pinned_machine_version
($machine);
1136 $conf->{machine
} = PVE
::QemuServer
::Machine
::print_machine
($machine_conf);
1139 PVE
::QemuServer
::Machine
::assert_valid_machine_property
($conf, $machine_conf);
1141 $conf->{lock} = 'import' if $live_import_mapping;
1143 PVE
::QemuConfig-
>write_config($vmid, $conf);
1148 foreach my $volid (@$vollist) {
1149 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
1155 PVE
::AccessControl
::add_vm_to_pool
($vmid, $pool) if $pool;
1157 PVE
::QemuServer
::create_ifaces_ipams_ips
($conf, $vmid);
1160 PVE
::QemuConfig-
>lock_config_full($vmid, 1, $realcmd);
1162 if ($start_after_create && !$live_restore) {
1163 print "Execute autostart\n";
1164 eval { PVE
::API2
::Qemu-
>vm_start({vmid
=> $vmid, node
=> $node}) };
1168 return $live_import_mapping;
1172 my ($code, $worker_name);
1174 $worker_name = 'qmrestore';
1176 eval { $restorefn->() };
1178 eval { PVE
::QemuConfig-
>remove_lock($vmid, 'create') };
1180 if ($restored_data) {
1181 warn "error after data was restored, VM disks should be OK but config may "
1182 ."require adaptions. VM $vmid state is NOT cleaned up.\n";
1184 warn "error before or during data restore, some or all disks were not "
1185 ."completely restored. VM $vmid state is NOT cleaned up.\n";
1191 $worker_name = 'qmcreate';
1193 # If a live import was requested the create function returns
1194 # the mapping for the startup.
1195 my $live_import_mapping = eval { $createfn->() };
1198 my $conffile = PVE
::QemuConfig-
>config_file($vmid);
1199 unlink($conffile) or die "failed to remove config file: $!\n";
1205 if ($live_import_mapping) {
1206 my $import_options = {
1207 bwlimit
=> $bwlimit,
1211 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1212 PVE
::QemuServer
::live_import_from_files
(
1213 $live_import_mapping,
1222 return $rpcenv->fork_worker($worker_name, $vmid, $authuser, $code);
1225 __PACKAGE__-
>register_method({
1230 description
=> "Directory index",
1235 additionalProperties
=> 0,
1237 node
=> get_standard_option
('pve-node'),
1238 vmid
=> get_standard_option
('pve-vmid'),
1246 subdir
=> { type
=> 'string' },
1249 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
1255 { subdir
=> 'config' },
1256 { subdir
=> 'cloudinit' },
1257 { subdir
=> 'pending' },
1258 { subdir
=> 'status' },
1259 { subdir
=> 'unlink' },
1260 { subdir
=> 'vncproxy' },
1261 { subdir
=> 'termproxy' },
1262 { subdir
=> 'migrate' },
1263 { subdir
=> 'resize' },
1264 { subdir
=> 'move' },
1265 { subdir
=> 'rrd' },
1266 { subdir
=> 'rrddata' },
1267 { subdir
=> 'monitor' },
1268 { subdir
=> 'agent' },
1269 { subdir
=> 'snapshot' },
1270 { subdir
=> 'spiceproxy' },
1271 { subdir
=> 'sendkey' },
1272 { subdir
=> 'firewall' },
1273 { subdir
=> 'mtunnel' },
1274 { subdir
=> 'remote_migrate' },
1280 __PACKAGE__-
>register_method ({
1281 subclass
=> "PVE::API2::Firewall::VM",
1282 path
=> '{vmid}/firewall',
1285 __PACKAGE__-
>register_method ({
1286 subclass
=> "PVE::API2::Qemu::Agent",
1287 path
=> '{vmid}/agent',
1290 __PACKAGE__-
>register_method({
1292 path
=> '{vmid}/rrd',
1294 protected
=> 1, # fixme: can we avoid that?
1296 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1298 description
=> "Read VM RRD statistics (returns PNG)",
1300 additionalProperties
=> 0,
1302 node
=> get_standard_option
('pve-node'),
1303 vmid
=> get_standard_option
('pve-vmid'),
1305 description
=> "Specify the time frame you are interested in.",
1307 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
1310 description
=> "The list of datasources you want to display.",
1311 type
=> 'string', format
=> 'pve-configid-list',
1314 description
=> "The RRD consolidation function",
1316 enum
=> [ 'AVERAGE', 'MAX' ],
1324 filename
=> { type
=> 'string' },
1330 return PVE
::RRD
::create_rrd_graph
(
1331 "pve2-vm/$param->{vmid}", $param->{timeframe
},
1332 $param->{ds
}, $param->{cf
});
1336 __PACKAGE__-
>register_method({
1338 path
=> '{vmid}/rrddata',
1340 protected
=> 1, # fixme: can we avoid that?
1342 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1344 description
=> "Read VM RRD statistics",
1346 additionalProperties
=> 0,
1348 node
=> get_standard_option
('pve-node'),
1349 vmid
=> get_standard_option
('pve-vmid'),
1351 description
=> "Specify the time frame you are interested in.",
1353 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
1356 description
=> "The RRD consolidation function",
1358 enum
=> [ 'AVERAGE', 'MAX' ],
1373 return PVE
::RRD
::create_rrd_data
(
1374 "pve2-vm/$param->{vmid}", $param->{timeframe
}, $param->{cf
});
1378 __PACKAGE__-
>register_method({
1379 name
=> 'vm_config',
1380 path
=> '{vmid}/config',
1383 description
=> "Get the virtual machine configuration with pending configuration " .
1384 "changes applied. Set the 'current' parameter to get the current configuration instead.",
1386 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1389 additionalProperties
=> 0,
1391 node
=> get_standard_option
('pve-node'),
1392 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1394 description
=> "Get current values (instead of pending values).",
1399 snapshot
=> get_standard_option
('pve-snapshot-name', {
1400 description
=> "Fetch config values from given snapshot.",
1403 my ($cmd, $pname, $cur, $args) = @_;
1404 PVE
::QemuConfig-
>snapshot_list($args->[0]);
1410 description
=> "The VM configuration.",
1412 properties
=> PVE
::QemuServer
::json_config_properties
({
1415 description
=> 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
1422 raise_param_exc
({ snapshot
=> "cannot use 'snapshot' parameter with 'current'",
1423 current
=> "cannot use 'snapshot' parameter with 'current'"})
1424 if ($param->{snapshot
} && $param->{current
});
1427 if ($param->{snapshot
}) {
1428 $conf = PVE
::QemuConfig-
>load_snapshot_config($param->{vmid
}, $param->{snapshot
});
1430 $conf = PVE
::QemuConfig-
>load_current_config($param->{vmid
}, $param->{current
});
1432 $conf->{cipassword
} = '**********' if $conf->{cipassword
};
1437 __PACKAGE__-
>register_method({
1438 name
=> 'vm_pending',
1439 path
=> '{vmid}/pending',
1442 description
=> "Get the virtual machine configuration with both current and pending values.",
1444 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1447 additionalProperties
=> 0,
1449 node
=> get_standard_option
('pve-node'),
1450 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1459 description
=> "Configuration option name.",
1463 description
=> "Current value.",
1468 description
=> "Pending value.",
1473 description
=> "Indicates a pending delete request if present and not 0. " .
1474 "The value 2 indicates a force-delete request.",
1486 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
1488 my $pending_delete_hash = PVE
::QemuConfig-
>parse_pending_delete($conf->{pending
}->{delete});
1490 $conf->{cipassword
} = '**********' if defined($conf->{cipassword
});
1491 $conf->{pending
}->{cipassword
} = '********** ' if defined($conf->{pending
}->{cipassword
});
1493 return PVE
::GuestHelpers
::config_with_pending_array
($conf, $pending_delete_hash);
1496 __PACKAGE__-
>register_method({
1497 name
=> 'cloudinit_pending',
1498 path
=> '{vmid}/cloudinit',
1501 description
=> "Get the cloudinit configuration with both current and pending values.",
1503 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1506 additionalProperties
=> 0,
1508 node
=> get_standard_option
('pve-node'),
1509 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1518 description
=> "Configuration option name.",
1522 description
=> "Value as it was used to generate the current cloudinit image.",
1527 description
=> "The new pending value.",
1532 description
=> "Indicates a pending delete request if present and not 0. ",
1544 my $vmid = $param->{vmid
};
1545 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1547 my $ci = $conf->{cloudinit
};
1549 $conf->{cipassword
} = '**********' if exists $conf->{cipassword
};
1550 $ci->{cipassword
} = '**********' if exists $ci->{cipassword
};
1554 # All the values that got added
1555 my $added = delete($ci->{added
}) // '';
1556 for my $key (PVE
::Tools
::split_list
($added)) {
1557 push @$res, { key
=> $key, pending
=> $conf->{$key} };
1560 # All already existing values (+ their new value, if it exists)
1561 for my $opt (keys %$cloudinitoptions) {
1562 next if !$conf->{$opt};
1563 next if $added =~ m/$opt/;
1568 if (my $pending = $ci->{$opt}) {
1569 $item->{value
} = $pending;
1570 $item->{pending
} = $conf->{$opt};
1572 $item->{value
} = $conf->{$opt},
1578 # Now, we'll find the deleted ones
1579 for my $opt (keys %$ci) {
1580 next if $conf->{$opt};
1581 push @$res, { key
=> $opt, delete => 1 };
1587 __PACKAGE__-
>register_method({
1588 name
=> 'cloudinit_update',
1589 path
=> '{vmid}/cloudinit',
1593 description
=> "Regenerate and change cloudinit config drive.",
1595 check
=> ['perm', '/vms/{vmid}', ['VM.Config.Cloudinit']],
1598 additionalProperties
=> 0,
1600 node
=> get_standard_option
('pve-node'),
1601 vmid
=> get_standard_option
('pve-vmid'),
1604 returns
=> { type
=> 'null' },
1608 my $rpcenv = PVE
::RPCEnvironment
::get
();
1609 my $authuser = $rpcenv->get_user();
1611 my $vmid = extract_param
($param, 'vmid');
1613 PVE
::QemuConfig-
>lock_config($vmid, sub {
1614 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1615 PVE
::QemuConfig-
>check_lock($conf);
1617 my $storecfg = PVE
::Storage
::config
();
1618 PVE
::QemuServer
::vmconfig_update_cloudinit_drive
($storecfg, $conf, $vmid);
1623 # POST/PUT {vmid}/config implementation
1625 # The original API used PUT (idempotent) an we assumed that all operations
1626 # are fast. But it turned out that almost any configuration change can
1627 # involve hot-plug actions, or disk alloc/free. Such actions can take long
1628 # time to complete and have side effects (not idempotent).
1630 # The new implementation uses POST and forks a worker process. We added
1631 # a new option 'background_delay'. If specified we wait up to
1632 # 'background_delay' second for the worker task to complete. It returns null
1633 # if the task is finished within that time, else we return the UPID.
1635 my $update_vm_api = sub {
1636 my ($param, $sync) = @_;
1638 my $rpcenv = PVE
::RPCEnvironment
::get
();
1640 my $authuser = $rpcenv->get_user();
1642 my $node = extract_param
($param, 'node');
1644 my $vmid = extract_param
($param, 'vmid');
1646 my $digest = extract_param
($param, 'digest');
1648 my $background_delay = extract_param
($param, 'background_delay');
1650 my $skip_cloud_init = extract_param
($param, 'skip_cloud_init');
1652 if (defined(my $cipassword = $param->{cipassword
})) {
1653 # Same logic as in cloud-init (but with the regex fixed...)
1654 $param->{cipassword
} = PVE
::Tools
::encrypt_pw
($cipassword)
1655 if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
1658 my @paramarr = (); # used for log message
1659 foreach my $key (sort keys %$param) {
1660 my $value = $key eq 'cipassword' ?
'<hidden>' : $param->{$key};
1661 push @paramarr, "-$key", $value;
1664 my $skiplock = extract_param
($param, 'skiplock');
1665 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1666 if $skiplock && $authuser ne 'root@pam';
1668 my $delete_str = extract_param
($param, 'delete');
1670 my $revert_str = extract_param
($param, 'revert');
1672 my $force = extract_param
($param, 'force');
1674 if (defined(my $ssh_keys = $param->{sshkeys
})) {
1675 $ssh_keys = URI
::Escape
::uri_unescape
($ssh_keys);
1676 PVE
::Tools
::validate_ssh_public_keys
($ssh_keys);
1679 $param->{cpuunits
} = PVE
::CGroup
::clamp_cpu_shares
($param->{cpuunits
})
1680 if defined($param->{cpuunits
}); # clamp value depending on cgroup version
1682 die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
1684 my $storecfg = PVE
::Storage
::config
();
1686 &$resolve_cdrom_alias($param);
1688 # now try to verify all parameters
1691 foreach my $opt (PVE
::Tools
::split_list
($revert_str)) {
1692 if (!PVE
::QemuServer
::option_exists
($opt)) {
1693 raise_param_exc
({ revert
=> "unknown option '$opt'" });
1696 raise_param_exc
({ delete => "you can't use '-$opt' and " .
1697 "-revert $opt' at the same time" })
1698 if defined($param->{$opt});
1700 $revert->{$opt} = 1;
1704 foreach my $opt (PVE
::Tools
::split_list
($delete_str)) {
1705 $opt = 'ide2' if $opt eq 'cdrom';
1707 raise_param_exc
({ delete => "you can't use '-$opt' and " .
1708 "-delete $opt' at the same time" })
1709 if defined($param->{$opt});
1711 raise_param_exc
({ revert
=> "you can't use '-delete $opt' and " .
1712 "-revert $opt' at the same time" })
1715 if (!PVE
::QemuServer
::option_exists
($opt)) {
1716 raise_param_exc
({ delete => "unknown option '$opt'" });
1722 my $repl_conf = PVE
::ReplicationConfig-
>new();
1723 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
1724 my $check_replication = sub {
1726 return if !$is_replicated;
1727 my $volid = $drive->{file
};
1728 return if !$volid || !($drive->{replicate
}//1);
1729 return if PVE
::QemuServer
::drive_is_cdrom
($drive);
1731 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
1732 die "cannot add non-managed/pass-through volume to a replicated VM\n"
1733 if !defined($storeid);
1735 return if defined($volname) && $volname eq 'cloudinit';
1738 if ($volid =~ $PVE::QemuServer
::Drive
::NEW_DISK_RE
) {
1740 $format = $drive->{format
} || PVE
::Storage
::storage_default_format
($storecfg, $storeid);
1742 $format = (PVE
::Storage
::parse_volname
($storecfg, $volid))[6];
1744 return if PVE
::Storage
::storage_can_replicate
($storecfg, $storeid, $format);
1745 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
1746 return if $scfg->{shared
};
1747 die "cannot add non-replicatable volume to a replicated VM\n";
1750 $check_drive_param->($param, $storecfg, $check_replication);
1752 foreach my $opt (keys %$param) {
1753 if ($opt =~ m/^net(\d+)$/) {
1755 my $net = PVE
::QemuServer
::parse_net
($param->{$opt});
1756 $param->{$opt} = PVE
::QemuServer
::print_net
($net);
1757 } elsif ($opt eq 'vmgenid') {
1758 if ($param->{$opt} eq '1') {
1759 $param->{$opt} = PVE
::QemuServer
::generate_uuid
();
1761 } elsif ($opt eq 'hookscript') {
1762 eval { PVE
::GuestHelpers
::check_hookscript
($param->{$opt}, $storecfg); };
1763 raise_param_exc
({ $opt => $@ }) if $@;
1767 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]);
1769 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
1771 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
1773 PVE
::QemuServer
::check_bridge_access
($rpcenv, $authuser, $param);
1775 my $updatefn = sub {
1777 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1779 die "checksum missmatch (file change by other user?)\n"
1780 if $digest && $digest ne $conf->{digest
};
1782 &$check_cpu_model_access($rpcenv, $authuser, $param, $conf);
1784 # FIXME: 'suspended' lock should probabyl be a state or "weak" lock?!
1785 if (scalar(@delete) && grep { $_ eq 'vmstate'} @delete) {
1786 if (defined($conf->{lock}) && $conf->{lock} eq 'suspended') {
1787 delete $conf->{lock}; # for check lock check, not written out
1788 push @delete, 'lock'; # this is the real deal to write it out
1790 push @delete, 'runningmachine' if $conf->{runningmachine
};
1791 push @delete, 'runningcpu' if $conf->{runningcpu
};
1794 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
1796 foreach my $opt (keys %$revert) {
1797 if (defined($conf->{$opt})) {
1798 $param->{$opt} = $conf->{$opt};
1799 } elsif (defined($conf->{pending
}->{$opt})) {
1804 if ($param->{memory
} || defined($param->{balloon
})) {
1806 my $memory = $param->{memory
} || $conf->{pending
}->{memory
} || $conf->{memory
};
1807 my $maxmem = get_current_memory
($memory);
1808 my $balloon = defined($param->{balloon
}) ?
$param->{balloon
} : $conf->{pending
}->{balloon
} || $conf->{balloon
};
1810 die "balloon value too large (must be smaller than assigned memory)\n"
1811 if $balloon && $balloon > $maxmem;
1814 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: " . join (' ', @paramarr));
1818 print "update VM $vmid: " . join (' ', @paramarr) . "\n";
1820 # write updates to pending section
1822 my $modified = {}; # record what $option we modify
1825 if (my $boot = $conf->{boot
}) {
1826 my $bootcfg = PVE
::JSONSchema
::parse_property_string
('pve-qm-boot', $boot);
1827 @bootorder = PVE
::Tools
::split_list
($bootcfg->{order
}) if $bootcfg && $bootcfg->{order
};
1829 my $bootorder_deleted = grep {$_ eq 'bootorder'} @delete;
1831 my $check_drive_perms = sub {
1832 my ($opt, $val) = @_;
1833 my $drive = PVE
::QemuServer
::parse_drive
($opt, $val, 1);
1834 if (PVE
::QemuServer
::drive_is_cloudinit
($drive)) {
1835 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Cloudinit', 'VM.Config.CDROM']);
1836 } elsif (PVE
::QemuServer
::drive_is_cdrom
($drive, 1)) { # CDROM
1837 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1839 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1844 foreach my $opt (@delete) {
1845 $modified->{$opt} = 1;
1846 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1848 # value of what we want to delete, independent if pending or not
1849 my $val = $conf->{$opt} // $conf->{pending
}->{$opt};
1850 if (!defined($val)) {
1851 warn "cannot delete '$opt' - not set in current configuration!\n";
1852 $modified->{$opt} = 0;
1855 my $is_pending_val = defined($conf->{pending
}->{$opt});
1856 delete $conf->{pending
}->{$opt};
1858 # remove from bootorder if necessary
1859 if (!$bootorder_deleted && @bootorder && grep {$_ eq $opt} @bootorder) {
1860 @bootorder = grep {$_ ne $opt} @bootorder;
1861 $conf->{pending
}->{boot
} = PVE
::QemuServer
::print_bootorder
(\
@bootorder);
1862 $modified->{boot
} = 1;
1865 if ($opt =~ m/^unused/) {
1866 my $drive = PVE
::QemuServer
::parse_drive
($opt, $val);
1867 PVE
::QemuConfig-
>check_protection($conf, "can't remove unused disk '$drive->{file}'");
1868 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1869 if (PVE
::QemuServer
::try_deallocate_drive
($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1870 delete $conf->{$opt};
1871 PVE
::QemuConfig-
>write_config($vmid, $conf);
1873 } elsif ($opt eq 'vmstate') {
1874 PVE
::QemuConfig-
>check_protection($conf, "can't remove vmstate '$val'");
1875 if (PVE
::QemuServer
::try_deallocate_drive
($storecfg, $vmid, $conf, $opt, { file
=> $val }, $rpcenv, $authuser, 1)) {
1876 delete $conf->{$opt};
1877 PVE
::QemuConfig-
>write_config($vmid, $conf);
1879 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
1880 PVE
::QemuConfig-
>check_protection($conf, "can't remove drive '$opt'");
1881 $check_drive_perms->($opt, $val);
1882 PVE
::QemuServer
::vmconfig_register_unused_drive
($storecfg, $vmid, $conf, PVE
::QemuServer
::parse_drive
($opt, $val))
1884 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1885 PVE
::QemuConfig-
>write_config($vmid, $conf);
1886 } elsif ($opt =~ m/^serial\d+$/) {
1887 if ($val eq 'socket') {
1888 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1889 } elsif ($authuser ne 'root@pam') {
1890 die "only root can delete '$opt' config for real devices\n";
1892 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1893 PVE
::QemuConfig-
>write_config($vmid, $conf);
1894 } elsif ($opt =~ m/^usb\d+$/) {
1895 check_usb_perm
($rpcenv, $authuser, $vmid, undef, $opt, $val);
1896 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1897 PVE
::QemuConfig-
>write_config($vmid, $conf);
1898 } elsif ($opt =~ m/^hostpci\d+$/) {
1899 check_hostpci_perm
($rpcenv, $authuser, $vmid, undef, $opt, $val);
1900 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1901 PVE
::QemuConfig-
>write_config($vmid, $conf);
1902 } elsif ($opt eq 'tags') {
1903 assert_tag_permissions
($vmid, $val, '', $rpcenv, $authuser);
1904 delete $conf->{$opt};
1905 PVE
::QemuConfig-
>write_config($vmid, $conf);
1906 } elsif ($opt =~ m/^net\d+$/) {
1907 if ($conf->{$opt}) {
1908 PVE
::QemuServer
::check_bridge_access
(
1911 { $opt => $conf->{$opt} },
1914 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1915 PVE
::QemuConfig-
>write_config($vmid, $conf);
1917 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1918 PVE
::QemuConfig-
>write_config($vmid, $conf);
1922 foreach my $opt (keys %$param) { # add/change
1923 $modified->{$opt} = 1;
1924 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1925 next if defined($conf->{pending
}->{$opt}) && ($param->{$opt} eq $conf->{pending
}->{$opt}); # skip if nothing changed
1927 my $arch = PVE
::QemuServer
::get_vm_arch
($conf);
1929 if (PVE
::QemuServer
::is_valid_drivename
($opt)) {
1931 if ($conf->{$opt}) {
1932 $check_drive_perms->($opt, $conf->{$opt});
1936 $check_drive_perms->($opt, $param->{$opt});
1937 PVE
::QemuServer
::vmconfig_register_unused_drive
($storecfg, $vmid, $conf, PVE
::QemuServer
::parse_drive
($opt, $conf->{pending
}->{$opt}))
1938 if defined($conf->{pending
}->{$opt});
1940 assert_scsi_feature_compatibility
($opt, $conf, $storecfg, $param->{$opt})
1941 if $opt =~ m/^scsi\d+$/;
1943 my (undef, $created_opts) = create_disks
(
1951 {$opt => $param->{$opt}},
1955 $conf->{pending
}->{$_} = $created_opts->{$_} for keys $created_opts->%*;
1957 # default legacy boot order implies all cdroms anyway
1959 # append new CD drives to bootorder to mark them bootable
1960 my $drive = PVE
::QemuServer
::parse_drive
($opt, $param->{$opt}, 1);
1961 if (PVE
::QemuServer
::drive_is_cdrom
($drive, 1) && !grep(/^$opt$/, @bootorder)) {
1962 push @bootorder, $opt;
1963 $conf->{pending
}->{boot
} = PVE
::QemuServer
::print_bootorder
(\
@bootorder);
1964 $modified->{boot
} = 1;
1967 } elsif ($opt =~ m/^serial\d+/) {
1968 if ((!defined($conf->{$opt}) || $conf->{$opt} eq 'socket') && $param->{$opt} eq 'socket') {
1969 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1970 } elsif ($authuser ne 'root@pam') {
1971 die "only root can modify '$opt' config for real devices\n";
1973 $conf->{pending
}->{$opt} = $param->{$opt};
1974 } elsif ($opt eq 'vga') {
1975 my $vga = PVE
::QemuServer
::parse_vga
($param->{$opt});
1976 PVE
::QemuServer
::assert_clipboard_config
($vga);
1977 $conf->{pending
}->{$opt} = $param->{$opt};
1978 } elsif ($opt =~ m/^usb\d+/) {
1979 if (my $olddevice = $conf->{$opt}) {
1980 check_usb_perm
($rpcenv, $authuser, $vmid, undef, $opt, $conf->{$opt});
1982 check_usb_perm
($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
1983 $conf->{pending
}->{$opt} = $param->{$opt};
1984 } elsif ($opt =~ m/^hostpci\d+$/) {
1985 if (my $oldvalue = $conf->{$opt}) {
1986 check_hostpci_perm
($rpcenv, $authuser, $vmid, undef, $opt, $oldvalue);
1988 check_hostpci_perm
($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
1989 $conf->{pending
}->{$opt} = $param->{$opt};
1990 } elsif ($opt eq 'tags') {
1991 assert_tag_permissions
($vmid, $conf->{$opt}, $param->{$opt}, $rpcenv, $authuser);
1992 $conf->{pending
}->{$opt} = PVE
::GuestHelpers
::get_unique_tags
($param->{$opt});
1993 } elsif ($opt =~ m/^net\d+$/) {
1994 if ($conf->{$opt}) {
1995 PVE
::QemuServer
::check_bridge_access
(
1998 { $opt => $conf->{$opt} },
2001 $conf->{pending
}->{$opt} = $param->{$opt};
2002 } elsif ($opt eq 'machine') {
2003 my $machine_conf = PVE
::QemuServer
::Machine
::parse_machine
($param->{$opt});
2004 PVE
::QemuServer
::Machine
::assert_valid_machine_property
($conf, $machine_conf);
2005 $conf->{pending
}->{$opt} = $param->{$opt};
2007 $conf->{pending
}->{$opt} = $param->{$opt};
2009 if ($opt eq 'boot') {
2010 my $new_bootcfg = PVE
::JSONSchema
::parse_property_string
('pve-qm-boot', $param->{$opt});
2011 if ($new_bootcfg->{order
}) {
2012 my @devs = PVE
::Tools
::split_list
($new_bootcfg->{order
});
2013 for my $dev (@devs) {
2014 my $exists = $conf->{$dev} || $conf->{pending
}->{$dev} || $param->{$dev};
2015 my $deleted = grep {$_ eq $dev} @delete;
2016 die "invalid bootorder: device '$dev' does not exist'\n"
2017 if !$exists || $deleted;
2020 # remove legacy boot order settings if new one set
2021 $conf->{pending
}->{$opt} = PVE
::QemuServer
::print_bootorder
(\
@devs);
2022 PVE
::QemuConfig-
>add_to_pending_delete($conf, "bootdisk")
2023 if $conf->{bootdisk
};
2027 PVE
::QemuConfig-
>remove_from_pending_delete($conf, $opt);
2028 PVE
::QemuConfig-
>write_config($vmid, $conf);
2031 # remove pending changes when nothing changed
2032 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
2033 my $changes = PVE
::QemuConfig-
>cleanup_pending($conf);
2034 PVE
::QemuConfig-
>write_config($vmid, $conf) if $changes;
2036 return if !scalar(keys %{$conf->{pending
}});
2038 my $running = PVE
::QemuServer
::check_running
($vmid);
2040 # apply pending changes
2042 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
2046 PVE
::QemuServer
::vmconfig_hotplug_pending
($vmid, $conf, $storecfg, $modified, $errors);
2048 # cloud_init must be skipped if we are in an incoming, remote live migration
2049 PVE
::QemuServer
::vmconfig_apply_pending
($vmid, $conf, $storecfg, $errors, $skip_cloud_init);
2051 raise_param_exc
($errors) if scalar(keys %$errors);
2060 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
2062 if ($background_delay) {
2064 # Note: It would be better to do that in the Event based HTTPServer
2065 # to avoid blocking call to sleep.
2067 my $end_time = time() + $background_delay;
2069 my $task = PVE
::Tools
::upid_decode
($upid);
2072 while (time() < $end_time) {
2073 $running = PVE
::ProcFSTools
::check_process_running
($task->{pid
}, $task->{pstart
});
2075 sleep(1); # this gets interrupted when child process ends
2079 my $status = PVE
::Tools
::upid_read_status
($upid);
2080 return if !PVE
::Tools
::upid_status_is_error
($status);
2081 die "failed to update VM $vmid: $status\n";
2089 return PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
2092 my $vm_config_perm_list = [
2097 'VM.Config.Network',
2099 'VM.Config.Options',
2100 'VM.Config.Cloudinit',
2103 __PACKAGE__-
>register_method({
2104 name
=> 'update_vm_async',
2105 path
=> '{vmid}/config',
2109 description
=> "Set virtual machine options (asynchrounous API).",
2111 check
=> ['perm', '/vms/{vmid}', $vm_config_perm_list, any
=> 1],
2114 additionalProperties
=> 0,
2115 properties
=> PVE
::QemuServer
::json_config_properties
(
2117 node
=> get_standard_option
('pve-node'),
2118 vmid
=> get_standard_option
('pve-vmid'),
2119 skiplock
=> get_standard_option
('skiplock'),
2121 type
=> 'string', format
=> 'pve-configid-list',
2122 description
=> "A list of settings you want to delete.",
2126 type
=> 'string', format
=> 'pve-configid-list',
2127 description
=> "Revert a pending change.",
2132 description
=> $opt_force_description,
2134 requires
=> 'delete',
2138 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2142 background_delay
=> {
2144 description
=> "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
2150 1, # with_disk_alloc
2157 code
=> $update_vm_api,
2160 __PACKAGE__-
>register_method({
2161 name
=> 'update_vm',
2162 path
=> '{vmid}/config',
2166 description
=> "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
2168 check
=> ['perm', '/vms/{vmid}', $vm_config_perm_list, any
=> 1],
2171 additionalProperties
=> 0,
2172 properties
=> PVE
::QemuServer
::json_config_properties
(
2174 node
=> get_standard_option
('pve-node'),
2175 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2176 skiplock
=> get_standard_option
('skiplock'),
2178 type
=> 'string', format
=> 'pve-configid-list',
2179 description
=> "A list of settings you want to delete.",
2183 type
=> 'string', format
=> 'pve-configid-list',
2184 description
=> "Revert a pending change.",
2189 description
=> $opt_force_description,
2191 requires
=> 'delete',
2195 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2200 1, # with_disk_alloc
2203 returns
=> { type
=> 'null' },
2206 &$update_vm_api($param, 1);
2211 __PACKAGE__-
>register_method({
2212 name
=> 'destroy_vm',
2217 description
=> "Destroy the VM and all used/owned volumes. Removes any VM specific permissions"
2218 ." and firewall rules",
2220 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
2223 additionalProperties
=> 0,
2225 node
=> get_standard_option
('pve-node'),
2226 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
2227 skiplock
=> get_standard_option
('skiplock'),
2230 description
=> "Remove VMID from configurations, like backup & replication jobs and HA.",
2233 'destroy-unreferenced-disks' => {
2235 description
=> "If set, destroy additionally all disks not referenced in the config"
2236 ." but with a matching VMID from all enabled storages.",
2248 my $rpcenv = PVE
::RPCEnvironment
::get
();
2249 my $authuser = $rpcenv->get_user();
2250 my $vmid = $param->{vmid
};
2252 my $skiplock = $param->{skiplock
};
2253 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2254 if $skiplock && $authuser ne 'root@pam';
2256 my $early_checks = sub {
2258 my $conf = PVE
::QemuConfig-
>load_config($vmid);
2259 PVE
::QemuConfig-
>check_protection($conf, "can't remove VM $vmid");
2261 my $ha_managed = PVE
::HA
::Config
::service_is_configured
("vm:$vmid");
2263 if (!$param->{purge
}) {
2264 die "unable to remove VM $vmid - used in HA resources and purge parameter not set.\n"
2266 # don't allow destroy if with replication jobs but no purge param
2267 my $repl_conf = PVE
::ReplicationConfig-
>new();
2268 $repl_conf->check_for_existing_jobs($vmid);
2271 die "VM $vmid is running - destroy failed\n"
2272 if PVE
::QemuServer
::check_running
($vmid);
2282 my $storecfg = PVE
::Storage
::config
();
2284 syslog
('info', "destroy VM $vmid: $upid\n");
2285 PVE
::QemuConfig-
>lock_config($vmid, sub {
2286 # repeat, config might have changed
2287 my $ha_managed = $early_checks->();
2289 my $purge_unreferenced = $param->{'destroy-unreferenced-disks'};
2291 PVE
::QemuServer
::destroy_vm
(
2294 $skiplock, { lock => 'destroyed' },
2295 $purge_unreferenced,
2298 PVE
::AccessControl
::remove_vm_access
($vmid);
2299 PVE
::Firewall
::remove_vmfw_conf
($vmid);
2300 if ($param->{purge
}) {
2301 print "purging VM $vmid from related configurations..\n";
2302 PVE
::ReplicationConfig
::remove_vmid_jobs
($vmid);
2303 PVE
::VZDump
::Plugin
::remove_vmid_from_backup_jobs
($vmid);
2306 PVE
::HA
::Config
::delete_service_from_config
("vm:$vmid");
2307 print "NOTE: removed VM $vmid from HA resource configuration.\n";
2311 # only now remove the zombie config, else we can have reuse race
2312 PVE
::QemuConfig-
>destroy_config($vmid);
2316 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
2319 __PACKAGE__-
>register_method({
2321 path
=> '{vmid}/unlink',
2325 description
=> "Unlink/delete disk images.",
2327 check
=> [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
2330 additionalProperties
=> 0,
2332 node
=> get_standard_option
('pve-node'),
2333 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2335 type
=> 'string', format
=> 'pve-configid-list',
2336 description
=> "A list of disk IDs you want to delete.",
2340 description
=> $opt_force_description,
2345 returns
=> { type
=> 'null'},
2349 $param->{delete} = extract_param
($param, 'idlist');
2351 __PACKAGE__-
>update_vm($param);
2356 # uses good entropy, each char is limited to 6 bit to get printable chars simply
2357 my $gen_rand_chars = sub {
2360 die "invalid length $length" if $length < 1;
2362 my $min = ord('!'); # first printable ascii
2364 my $rand_bytes = Crypt
::OpenSSL
::Random
::random_bytes
($length);
2365 die "failed to generate random bytes!\n"
2368 my $str = join('', map { chr((ord($_) & 0x3F) + $min) } split('', $rand_bytes));
2375 __PACKAGE__-
>register_method({
2377 path
=> '{vmid}/vncproxy',
2381 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2383 description
=> "Creates a TCP VNC proxy connections.",
2385 additionalProperties
=> 0,
2387 node
=> get_standard_option
('pve-node'),
2388 vmid
=> get_standard_option
('pve-vmid'),
2392 description
=> "Prepare for websocket upgrade (only required when using "
2393 ."serial terminal, otherwise upgrade is always possible).",
2395 'generate-password' => {
2399 description
=> "Generates a random password to be used as ticket instead of the API ticket.",
2404 additionalProperties
=> 0,
2406 user
=> { type
=> 'string' },
2407 ticket
=> { type
=> 'string' },
2410 description
=> "Returned if requested with 'generate-password' param."
2411 ." Consists of printable ASCII characters ('!' .. '~').",
2414 cert
=> { type
=> 'string' },
2415 port
=> { type
=> 'integer' },
2416 upid
=> { type
=> 'string' },
2422 my $rpcenv = PVE
::RPCEnvironment
::get
();
2424 my $authuser = $rpcenv->get_user();
2426 my $vmid = $param->{vmid
};
2427 my $node = $param->{node
};
2428 my $websocket = $param->{websocket
};
2430 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # check if VM exists
2434 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2435 $serial = $vga->{type
} if $vga->{type
} =~ m/^serial\d+$/;
2438 my $authpath = "/vms/$vmid";
2440 my $ticket = PVE
::AccessControl
::assemble_vnc_ticket
($authuser, $authpath);
2441 my $password = $ticket;
2442 if ($param->{'generate-password'}) {
2443 $password = $gen_rand_chars->(8);
2446 $sslcert = PVE
::Tools
::file_get_contents
("/etc/pve/pve-root-ca.pem", 8192)
2452 if ($node ne 'localhost' && $node ne PVE
::INotify
::nodename
()) {
2453 (undef, $family) = PVE
::Cluster
::remote_node_ip
($node);
2454 my $sshinfo = PVE
::SSHInfo
::get_ssh_info
($node);
2455 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
2456 $remcmd = PVE
::SSHInfo
::ssh_info_to_command
($sshinfo, defined($serial) ?
'-t' : '-T');
2458 $family = PVE
::Tools
::get_host_address_family
($node);
2461 my $port = PVE
::Tools
::next_vnc_port
($family);
2468 syslog
('info', "starting vnc proxy $upid\n");
2472 if (defined($serial)) {
2474 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $serial, '-escape', '0' ];
2476 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
2477 '-timeout', $timeout, '-authpath', $authpath,
2478 '-perm', 'Sys.Console'];
2480 if ($param->{websocket
}) {
2481 $ENV{PVE_VNC_TICKET
} = $password; # pass ticket to vncterm
2482 push @$cmd, '-notls', '-listen', 'localhost';
2485 push @$cmd, '-c', @$remcmd, @$termcmd;
2487 PVE
::Tools
::run_command
($cmd);
2491 $ENV{LC_PVE_TICKET
} = $password; # set ticket with "qm vncproxy"
2493 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
2495 my $sock = IO
::Socket
::IP-
>new(
2500 GetAddrInfoFlags
=> 0,
2501 ) or die "failed to create socket: $!\n";
2502 # Inside the worker we shouldn't have any previous alarms
2503 # running anyway...:
2505 local $SIG{ALRM
} = sub { die "connection timed out\n" };
2507 accept(my $cli, $sock) or die "connection failed: $!\n";
2510 if (PVE
::Tools
::run_command
($cmd,
2511 output
=> '>&'.fileno($cli),
2512 input
=> '<&'.fileno($cli),
2515 die "Failed to run vncproxy.\n";
2522 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
2524 PVE
::Tools
::wait_for_vnc_port
($port);
2533 $res->{password
} = $password if $param->{'generate-password'};
2538 __PACKAGE__-
>register_method({
2539 name
=> 'termproxy',
2540 path
=> '{vmid}/termproxy',
2544 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2546 description
=> "Creates a TCP proxy connections.",
2548 additionalProperties
=> 0,
2550 node
=> get_standard_option
('pve-node'),
2551 vmid
=> get_standard_option
('pve-vmid'),
2555 enum
=> [qw(serial0 serial1 serial2 serial3)],
2556 description
=> "opens a serial terminal (defaults to display)",
2561 additionalProperties
=> 0,
2563 user
=> { type
=> 'string' },
2564 ticket
=> { type
=> 'string' },
2565 port
=> { type
=> 'integer' },
2566 upid
=> { type
=> 'string' },
2572 my $rpcenv = PVE
::RPCEnvironment
::get
();
2574 my $authuser = $rpcenv->get_user();
2576 my $vmid = $param->{vmid
};
2577 my $node = $param->{node
};
2578 my $serial = $param->{serial
};
2580 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # check if VM exists
2582 if (!defined($serial)) {
2584 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2585 $serial = $vga->{type
} if $vga->{type
} =~ m/^serial\d+$/;
2589 my $authpath = "/vms/$vmid";
2591 my $ticket = PVE
::AccessControl
::assemble_vnc_ticket
($authuser, $authpath);
2596 if ($node ne 'localhost' && $node ne PVE
::INotify
::nodename
()) {
2597 (undef, $family) = PVE
::Cluster
::remote_node_ip
($node);
2598 my $sshinfo = PVE
::SSHInfo
::get_ssh_info
($node);
2599 $remcmd = PVE
::SSHInfo
::ssh_info_to_command
($sshinfo, '-t');
2600 push @$remcmd, '--';
2602 $family = PVE
::Tools
::get_host_address_family
($node);
2605 my $port = PVE
::Tools
::next_vnc_port
($family);
2607 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-escape', '0'];
2608 push @$termcmd, '-iface', $serial if $serial;
2613 syslog
('info', "starting qemu termproxy $upid\n");
2615 my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath,
2616 '--perm', 'VM.Console', '--'];
2617 push @$cmd, @$remcmd, @$termcmd;
2619 PVE
::Tools
::run_command
($cmd);
2622 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
2624 PVE
::Tools
::wait_for_vnc_port
($port);
2634 __PACKAGE__-
>register_method({
2635 name
=> 'vncwebsocket',
2636 path
=> '{vmid}/vncwebsocket',
2639 description
=> "You also need to pass a valid ticket (vncticket).",
2640 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2642 description
=> "Opens a weksocket for VNC traffic.",
2644 additionalProperties
=> 0,
2646 node
=> get_standard_option
('pve-node'),
2647 vmid
=> get_standard_option
('pve-vmid'),
2649 description
=> "Ticket from previous call to vncproxy.",
2654 description
=> "Port number returned by previous vncproxy call.",
2664 port
=> { type
=> 'string' },
2670 my $rpcenv = PVE
::RPCEnvironment
::get
();
2672 my $authuser = $rpcenv->get_user();
2674 my $vmid = $param->{vmid
};
2675 my $node = $param->{node
};
2677 my $authpath = "/vms/$vmid";
2679 PVE
::AccessControl
::verify_vnc_ticket
($param->{vncticket
}, $authuser, $authpath);
2681 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # VM exists ?
2683 # Note: VNC ports are acessible from outside, so we do not gain any
2684 # security if we verify that $param->{port} belongs to VM $vmid. This
2685 # check is done by verifying the VNC ticket (inside VNC protocol).
2687 my $port = $param->{port
};
2689 return { port
=> $port };
2692 __PACKAGE__-
>register_method({
2693 name
=> 'spiceproxy',
2694 path
=> '{vmid}/spiceproxy',
2699 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2701 description
=> "Returns a SPICE configuration to connect to the VM.",
2703 additionalProperties
=> 0,
2705 node
=> get_standard_option
('pve-node'),
2706 vmid
=> get_standard_option
('pve-vmid'),
2707 proxy
=> get_standard_option
('spice-proxy', { optional
=> 1 }),
2710 returns
=> get_standard_option
('remote-viewer-config'),
2714 my $rpcenv = PVE
::RPCEnvironment
::get
();
2716 my $authuser = $rpcenv->get_user();
2718 my $vmid = $param->{vmid
};
2719 my $node = $param->{node
};
2720 my $proxy = $param->{proxy
};
2722 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node);
2723 my $title = "VM $vmid";
2724 $title .= " - ". $conf->{name
} if $conf->{name
};
2726 my $port = PVE
::QemuServer
::spice_port
($vmid);
2728 my ($ticket, undef, $remote_viewer_config) =
2729 PVE
::AccessControl
::remote_viewer_config
($authuser, $vmid, $node, $proxy, $title, $port);
2731 mon_cmd
($vmid, "set_password", protocol
=> 'spice', password
=> $ticket);
2732 mon_cmd
($vmid, "expire_password", protocol
=> 'spice', time => "+30");
2734 return $remote_viewer_config;
2737 __PACKAGE__-
>register_method({
2739 path
=> '{vmid}/status',
2742 description
=> "Directory index",
2747 additionalProperties
=> 0,
2749 node
=> get_standard_option
('pve-node'),
2750 vmid
=> get_standard_option
('pve-vmid'),
2758 subdir
=> { type
=> 'string' },
2761 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
2767 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
2770 { subdir
=> 'current' },
2771 { subdir
=> 'start' },
2772 { subdir
=> 'stop' },
2773 { subdir
=> 'reset' },
2774 { subdir
=> 'shutdown' },
2775 { subdir
=> 'suspend' },
2776 { subdir
=> 'reboot' },
2782 __PACKAGE__-
>register_method({
2783 name
=> 'vm_status',
2784 path
=> '{vmid}/status/current',
2787 protected
=> 1, # qemu pid files are only readable by root
2788 description
=> "Get virtual machine status.",
2790 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2793 additionalProperties
=> 0,
2795 node
=> get_standard_option
('pve-node'),
2796 vmid
=> get_standard_option
('pve-vmid'),
2802 %$PVE::QemuServer
::vmstatus_return_properties
,
2804 description
=> "HA manager service status.",
2808 description
=> "QEMU VGA configuration supports spice.",
2813 description
=> "QEMU Guest Agent is enabled in config.",
2818 description
=> 'Enable a specific clipboard. If not set, depending on'
2819 .' the display type the SPICE one will be added.',
2830 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
2832 my $vmstatus = PVE
::QemuServer
::vmstatus
($param->{vmid
}, 1);
2833 my $status = $vmstatus->{$param->{vmid
}};
2835 $status->{ha
} = PVE
::HA
::Config
::get_service_status
("vm:$param->{vmid}");
2838 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2839 my $spice = defined($vga->{type
}) && $vga->{type
} =~ /^virtio/;
2840 $spice ||= PVE
::QemuServer
::vga_conf_has_spice
($conf->{vga
});
2841 $status->{spice
} = 1 if $spice;
2842 $status->{clipboard
} = $vga->{clipboard
};
2844 $status->{agent
} = 1 if PVE
::QemuServer
::get_qga_key
($conf, 'enabled');
2849 __PACKAGE__-
>register_method({
2851 path
=> '{vmid}/status/start',
2855 description
=> "Start virtual machine.",
2857 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2860 additionalProperties
=> 0,
2862 node
=> get_standard_option
('pve-node'),
2863 vmid
=> get_standard_option
('pve-vmid',
2864 { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
2865 skiplock
=> get_standard_option
('skiplock'),
2866 stateuri
=> get_standard_option
('pve-qm-stateuri'),
2867 migratedfrom
=> get_standard_option
('pve-node',{ optional
=> 1 }),
2870 enum
=> ['secure', 'insecure'],
2871 description
=> "Migration traffic is encrypted using an SSH " .
2872 "tunnel by default. On secure, completely private networks " .
2873 "this can be disabled to increase performance.",
2876 migration_network
=> {
2877 type
=> 'string', format
=> 'CIDR',
2878 description
=> "CIDR of the (sub) network that is used for migration.",
2881 machine
=> get_standard_option
('pve-qemu-machine'),
2883 description
=> "Override QEMU's -cpu argument with the given string.",
2887 targetstorage
=> get_standard_option
('pve-targetstorage'),
2889 description
=> "Wait maximal timeout seconds.",
2892 default => 'max(30, vm memory in GiB)',
2903 my $rpcenv = PVE
::RPCEnvironment
::get
();
2904 my $authuser = $rpcenv->get_user();
2906 my $node = extract_param
($param, 'node');
2907 my $vmid = extract_param
($param, 'vmid');
2908 my $timeout = extract_param
($param, 'timeout');
2909 my $machine = extract_param
($param, 'machine');
2911 my $get_root_param = sub {
2912 my $value = extract_param
($param, $_[0]);
2913 raise_param_exc
({ "$_[0]" => "Only root may use this option." })
2914 if $value && $authuser ne 'root@pam';
2918 my $stateuri = $get_root_param->('stateuri');
2919 my $skiplock = $get_root_param->('skiplock');
2920 my $migratedfrom = $get_root_param->('migratedfrom');
2921 my $migration_type = $get_root_param->('migration_type');
2922 my $migration_network = $get_root_param->('migration_network');
2923 my $targetstorage = $get_root_param->('targetstorage');
2924 my $force_cpu = $get_root_param->('force-cpu');
2928 if ($targetstorage) {
2929 raise_param_exc
({ targetstorage
=> "targetstorage can only by used with migratedfrom." })
2931 $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($targetstorage, 'pve-storage-id') };
2932 raise_param_exc
({ targetstorage
=> "failed to parse storage map: $@" })
2936 # read spice ticket from STDIN
2938 my $nbd_protocol_version = 0;
2939 my $replicated_volumes = {};
2940 my $offline_volumes = {};
2941 if ($stateuri && ($stateuri eq 'tcp' || $stateuri eq 'unix') && $migratedfrom && ($rpcenv->{type
} eq 'cli')) {
2942 while (defined(my $line = <STDIN
>)) {
2944 if ($line =~ m/^spice_ticket: (.+)$/) {
2946 } elsif ($line =~ m/^nbd_protocol_version: (\d+)$/) {
2947 $nbd_protocol_version = $1;
2948 } elsif ($line =~ m/^replicated_volume: (.*)$/) {
2949 $replicated_volumes->{$1} = 1;
2950 } elsif ($line =~ m/^tpmstate0: (.*)$/) { # Deprecated, use offline_volume instead
2951 $offline_volumes->{tpmstate0
} = $1;
2952 } elsif ($line =~ m/^offline_volume: ([^:]+): (.*)$/) {
2953 $offline_volumes->{$1} = $2;
2954 } elsif (!$spice_ticket) {
2955 # fallback for old source node
2956 $spice_ticket = $line;
2958 warn "unknown 'start' parameter on STDIN: '$line'\n";
2963 PVE
::Cluster
::check_cfs_quorum
();
2965 my $storecfg = PVE
::Storage
::config
();
2967 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && !$stateuri && $rpcenv->{type
} ne 'ha') {
2971 print "Requesting HA start for VM $vmid\n";
2973 my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'started'];
2974 PVE
::Tools
::run_command
($cmd);
2978 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
2985 syslog
('info', "start VM $vmid: $upid\n");
2987 my $migrate_opts = {
2988 migratedfrom
=> $migratedfrom,
2989 spice_ticket
=> $spice_ticket,
2990 network
=> $migration_network,
2991 type
=> $migration_type,
2992 storagemap
=> $storagemap,
2993 nbd_proto_version
=> $nbd_protocol_version,
2994 replicated_volumes
=> $replicated_volumes,
2995 offline_volumes
=> $offline_volumes,
2999 statefile
=> $stateuri,
3000 skiplock
=> $skiplock,
3001 forcemachine
=> $machine,
3002 timeout
=> $timeout,
3003 forcecpu
=> $force_cpu,
3006 PVE
::QemuServer
::vm_start
($storecfg, $vmid, $params, $migrate_opts);
3010 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
3014 __PACKAGE__-
>register_method({
3016 path
=> '{vmid}/status/stop',
3020 description
=> "Stop virtual machine. The qemu process will exit immediately. This" .
3021 "is akin to pulling the power plug of a running computer and may damage the VM data",
3023 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3026 additionalProperties
=> 0,
3028 node
=> get_standard_option
('pve-node'),
3029 vmid
=> get_standard_option
('pve-vmid',
3030 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3031 skiplock
=> get_standard_option
('skiplock'),
3032 migratedfrom
=> get_standard_option
('pve-node', { optional
=> 1 }),
3034 description
=> "Wait maximal timeout seconds.",
3040 description
=> "Do not deactivate storage volumes.",
3053 my $rpcenv = PVE
::RPCEnvironment
::get
();
3054 my $authuser = $rpcenv->get_user();
3056 my $node = extract_param
($param, 'node');
3057 my $vmid = extract_param
($param, 'vmid');
3059 my $skiplock = extract_param
($param, 'skiplock');
3060 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3061 if $skiplock && $authuser ne 'root@pam';
3063 my $keepActive = extract_param
($param, 'keepActive');
3064 raise_param_exc
({ keepActive
=> "Only root may use this option." })
3065 if $keepActive && $authuser ne 'root@pam';
3067 my $migratedfrom = extract_param
($param, 'migratedfrom');
3068 raise_param_exc
({ migratedfrom
=> "Only root may use this option." })
3069 if $migratedfrom && $authuser ne 'root@pam';
3072 my $storecfg = PVE
::Storage
::config
();
3074 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && ($rpcenv->{type
} ne 'ha') && !defined($migratedfrom)) {
3079 print "Requesting HA stop for VM $vmid\n";
3081 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", '0'];
3082 PVE
::Tools
::run_command
($cmd);
3086 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
3092 syslog
('info', "stop VM $vmid: $upid\n");
3094 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0,
3095 $param->{timeout
}, 0, 1, $keepActive, $migratedfrom);
3099 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
3103 __PACKAGE__-
>register_method({
3105 path
=> '{vmid}/status/reset',
3109 description
=> "Reset virtual machine.",
3111 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3114 additionalProperties
=> 0,
3116 node
=> get_standard_option
('pve-node'),
3117 vmid
=> get_standard_option
('pve-vmid',
3118 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3119 skiplock
=> get_standard_option
('skiplock'),
3128 my $rpcenv = PVE
::RPCEnvironment
::get
();
3130 my $authuser = $rpcenv->get_user();
3132 my $node = extract_param
($param, 'node');
3134 my $vmid = extract_param
($param, 'vmid');
3136 my $skiplock = extract_param
($param, 'skiplock');
3137 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3138 if $skiplock && $authuser ne 'root@pam';
3140 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3145 PVE
::QemuServer
::vm_reset
($vmid, $skiplock);
3150 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
3153 __PACKAGE__-
>register_method({
3154 name
=> 'vm_shutdown',
3155 path
=> '{vmid}/status/shutdown',
3159 description
=> "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
3160 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
3162 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3165 additionalProperties
=> 0,
3167 node
=> get_standard_option
('pve-node'),
3168 vmid
=> get_standard_option
('pve-vmid',
3169 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3170 skiplock
=> get_standard_option
('skiplock'),
3172 description
=> "Wait maximal timeout seconds.",
3178 description
=> "Make sure the VM stops.",
3184 description
=> "Do not deactivate storage volumes.",
3197 my $rpcenv = PVE
::RPCEnvironment
::get
();
3198 my $authuser = $rpcenv->get_user();
3200 my $node = extract_param
($param, 'node');
3201 my $vmid = extract_param
($param, 'vmid');
3203 my $skiplock = extract_param
($param, 'skiplock');
3204 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3205 if $skiplock && $authuser ne 'root@pam';
3207 my $keepActive = extract_param
($param, 'keepActive');
3208 raise_param_exc
({ keepActive
=> "Only root may use this option." })
3209 if $keepActive && $authuser ne 'root@pam';
3211 my $storecfg = PVE
::Storage
::config
();
3215 # sending a graceful shutdown command to paused VMs runs into timeouts, and even worse, when
3216 # the VM gets resumed later, it still gets the request delivered and powers off
3217 if (PVE
::QemuServer
::vm_is_paused
($vmid, 1)) {
3218 if ($param->{forceStop
}) {
3219 warn "VM is paused - stop instead of shutdown\n";
3222 die "VM is paused - cannot shutdown\n";
3226 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
3228 my $timeout = $param->{timeout
} // 60;
3232 print "Requesting HA stop for VM $vmid\n";
3234 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", "$timeout"];
3235 PVE
::Tools
::run_command
($cmd);
3239 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
3246 syslog
('info', "shutdown VM $vmid: $upid\n");
3248 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0, $param->{timeout
},
3249 $shutdown, $param->{forceStop
}, $keepActive);
3253 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
3257 __PACKAGE__-
>register_method({
3258 name
=> 'vm_reboot',
3259 path
=> '{vmid}/status/reboot',
3263 description
=> "Reboot the VM by shutting it down, and starting it again. Applies pending changes.",
3265 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3268 additionalProperties
=> 0,
3270 node
=> get_standard_option
('pve-node'),
3271 vmid
=> get_standard_option
('pve-vmid',
3272 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3274 description
=> "Wait maximal timeout seconds for the shutdown.",
3287 my $rpcenv = PVE
::RPCEnvironment
::get
();
3288 my $authuser = $rpcenv->get_user();
3290 my $node = extract_param
($param, 'node');
3291 my $vmid = extract_param
($param, 'vmid');
3293 die "VM is paused - cannot shutdown\n" if PVE
::QemuServer
::vm_is_paused
($vmid, 1);
3295 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3300 syslog
('info', "requesting reboot of VM $vmid: $upid\n");
3301 PVE
::QemuServer
::vm_reboot
($vmid, $param->{timeout
});
3305 return $rpcenv->fork_worker('qmreboot', $vmid, $authuser, $realcmd);
3308 __PACKAGE__-
>register_method({
3309 name
=> 'vm_suspend',
3310 path
=> '{vmid}/status/suspend',
3314 description
=> "Suspend virtual machine.",
3316 description
=> "You need 'VM.PowerMgmt' on /vms/{vmid}, and if you have set 'todisk',".
3317 " you need also 'VM.Config.Disk' on /vms/{vmid} and 'Datastore.AllocateSpace'".
3318 " on the storage for the vmstate.",
3319 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3322 additionalProperties
=> 0,
3324 node
=> get_standard_option
('pve-node'),
3325 vmid
=> get_standard_option
('pve-vmid',
3326 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3327 skiplock
=> get_standard_option
('skiplock'),
3332 description
=> 'If set, suspends the VM to disk. Will be resumed on next VM start.',
3334 statestorage
=> get_standard_option
('pve-storage-id', {
3335 description
=> "The storage for the VM state",
3336 requires
=> 'todisk',
3338 completion
=> \
&PVE
::Storage
::complete_storage_enabled
,
3348 my $rpcenv = PVE
::RPCEnvironment
::get
();
3349 my $authuser = $rpcenv->get_user();
3351 my $node = extract_param
($param, 'node');
3352 my $vmid = extract_param
($param, 'vmid');
3354 my $todisk = extract_param
($param, 'todisk') // 0;
3356 my $statestorage = extract_param
($param, 'statestorage');
3358 my $skiplock = extract_param
($param, 'skiplock');
3359 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3360 if $skiplock && $authuser ne 'root@pam';
3362 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3364 die "Cannot suspend HA managed VM to disk\n"
3365 if $todisk && PVE
::HA
::Config
::vm_is_ha_managed
($vmid);
3367 # early check for storage permission, for better user feedback
3369 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
3370 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3372 # cannot save the state of a non-virtualized PCIe device, so resume cannot really work
3373 for my $key (keys %$conf) {
3374 next if $key !~ /^hostpci\d+/;
3375 die "cannot suspend VM to disk due to passed-through PCI device(s), which lack the"
3376 ." possibility to save/restore their internal state\n";
3379 if (!$statestorage) {
3380 # get statestorage from config if none is given
3381 my $storecfg = PVE
::Storage
::config
();
3382 $statestorage = PVE
::QemuServer
::find_vmstate_storage
($conf, $storecfg);
3385 $rpcenv->check($authuser, "/storage/$statestorage", ['Datastore.AllocateSpace']);
3391 syslog
('info', "suspend VM $vmid: $upid\n");
3393 PVE
::QemuServer
::vm_suspend
($vmid, $skiplock, $todisk, $statestorage);
3398 my $taskname = $todisk ?
'qmsuspend' : 'qmpause';
3399 return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd);
3402 __PACKAGE__-
>register_method({
3403 name
=> 'vm_resume',
3404 path
=> '{vmid}/status/resume',
3408 description
=> "Resume virtual machine.",
3410 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3413 additionalProperties
=> 0,
3415 node
=> get_standard_option
('pve-node'),
3416 vmid
=> get_standard_option
('pve-vmid',
3417 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3418 skiplock
=> get_standard_option
('skiplock'),
3419 nocheck
=> { type
=> 'boolean', optional
=> 1 },
3429 my $rpcenv = PVE
::RPCEnvironment
::get
();
3431 my $authuser = $rpcenv->get_user();
3433 my $node = extract_param
($param, 'node');
3435 my $vmid = extract_param
($param, 'vmid');
3437 my $skiplock = extract_param
($param, 'skiplock');
3438 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3439 if $skiplock && $authuser ne 'root@pam';
3441 # nocheck is used as part of migration when config file might be still
3443 my $nocheck = extract_param
($param, 'nocheck');
3444 raise_param_exc
({ nocheck
=> "Only root may use this option." })
3445 if $nocheck && $authuser ne 'root@pam';
3447 my $to_disk_suspended;
3449 PVE
::QemuConfig-
>lock_config($vmid, sub {
3450 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3451 $to_disk_suspended = PVE
::QemuConfig-
>has_lock($conf, 'suspended');
3455 die "VM $vmid not running\n"
3456 if !$to_disk_suspended && !PVE
::QemuServer
::check_running
($vmid, $nocheck);
3461 syslog
('info', "resume VM $vmid: $upid\n");
3463 if (!$to_disk_suspended) {
3464 PVE
::QemuServer
::vm_resume
($vmid, $skiplock, $nocheck);
3466 my $storecfg = PVE
::Storage
::config
();
3467 PVE
::QemuServer
::vm_start
($storecfg, $vmid, { skiplock
=> $skiplock });
3473 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
3476 __PACKAGE__-
>register_method({
3477 name
=> 'vm_sendkey',
3478 path
=> '{vmid}/sendkey',
3482 description
=> "Send key event to virtual machine.",
3484 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
3487 additionalProperties
=> 0,
3489 node
=> get_standard_option
('pve-node'),
3490 vmid
=> get_standard_option
('pve-vmid',
3491 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3492 skiplock
=> get_standard_option
('skiplock'),
3494 description
=> "The key (qemu monitor encoding).",
3499 returns
=> { type
=> 'null'},
3503 my $rpcenv = PVE
::RPCEnvironment
::get
();
3505 my $authuser = $rpcenv->get_user();
3507 my $node = extract_param
($param, 'node');
3509 my $vmid = extract_param
($param, 'vmid');
3511 my $skiplock = extract_param
($param, 'skiplock');
3512 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3513 if $skiplock && $authuser ne 'root@pam';
3515 PVE
::QemuServer
::vm_sendkey
($vmid, $skiplock, $param->{key
});
3520 __PACKAGE__-
>register_method({
3521 name
=> 'vm_feature',
3522 path
=> '{vmid}/feature',
3526 description
=> "Check if feature for virtual machine is available.",
3528 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3531 additionalProperties
=> 0,
3533 node
=> get_standard_option
('pve-node'),
3534 vmid
=> get_standard_option
('pve-vmid'),
3536 description
=> "Feature to check.",
3538 enum
=> [ 'snapshot', 'clone', 'copy' ],
3540 snapname
=> get_standard_option
('pve-snapshot-name', {
3548 hasFeature
=> { type
=> 'boolean' },
3551 items
=> { type
=> 'string' },
3558 my $node = extract_param
($param, 'node');
3560 my $vmid = extract_param
($param, 'vmid');
3562 my $snapname = extract_param
($param, 'snapname');
3564 my $feature = extract_param
($param, 'feature');
3566 my $running = PVE
::QemuServer
::check_running
($vmid);
3568 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3571 my $snap = $conf->{snapshots
}->{$snapname};
3572 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3575 my $storecfg = PVE
::Storage
::config
();
3577 my $nodelist = PVE
::QemuServer
::shared_nodes
($conf, $storecfg);
3578 my $hasFeature = PVE
::QemuConfig-
>has_feature($feature, $conf, $storecfg, $snapname, $running);
3581 hasFeature
=> $hasFeature,
3582 nodes
=> [ keys %$nodelist ],
3586 __PACKAGE__-
>register_method({
3588 path
=> '{vmid}/clone',
3592 description
=> "Create a copy of virtual machine/template.",
3594 description
=> "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
3595 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
3596 "'Datastore.AllocateSpace' on any used storage and 'SDN.Use' on any used bridge/vnet",
3599 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
3601 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
3602 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param
=> 'pool'],
3607 additionalProperties
=> 0,
3609 node
=> get_standard_option
('pve-node'),
3610 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3611 newid
=> get_standard_option
('pve-vmid', {
3612 completion
=> \
&PVE
::Cluster
::complete_next_vmid
,
3613 description
=> 'VMID for the clone.' }),
3616 type
=> 'string', format
=> 'dns-name',
3617 description
=> "Set a name for the new VM.",
3622 description
=> "Description for the new VM.",
3626 type
=> 'string', format
=> 'pve-poolid',
3627 description
=> "Add the new VM to the specified pool.",
3629 snapname
=> get_standard_option
('pve-snapshot-name', {
3632 storage
=> get_standard_option
('pve-storage-id', {
3633 description
=> "Target storage for full clone.",
3637 description
=> "Target format for file storage. Only valid for full clone.",
3640 enum
=> [ 'raw', 'qcow2', 'vmdk'],
3645 description
=> "Create a full copy of all disks. This is always done when " .
3646 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
3648 target
=> get_standard_option
('pve-node', {
3649 description
=> "Target node. Only allowed if the original VM is on shared storage.",
3653 description
=> "Override I/O bandwidth limit (in KiB/s).",
3657 default => 'clone limit from datacenter or storage config',
3667 my $rpcenv = PVE
::RPCEnvironment
::get
();
3668 my $authuser = $rpcenv->get_user();
3670 my $node = extract_param
($param, 'node');
3671 my $vmid = extract_param
($param, 'vmid');
3672 my $newid = extract_param
($param, 'newid');
3673 my $pool = extract_param
($param, 'pool');
3675 my $snapname = extract_param
($param, 'snapname');
3676 my $storage = extract_param
($param, 'storage');
3677 my $format = extract_param
($param, 'format');
3678 my $target = extract_param
($param, 'target');
3680 my $localnode = PVE
::INotify
::nodename
();
3682 if ($target && ($target eq $localnode || $target eq 'localhost')) {
3686 my $running = PVE
::QemuServer
::check_running
($vmid) || 0;
3688 my $load_and_check = sub {
3689 $rpcenv->check_pool_exist($pool) if defined($pool);
3690 PVE
::Cluster
::check_node_exists
($target) if $target;
3692 my $storecfg = PVE
::Storage
::config
();
3695 # check if storage is enabled on local node
3696 PVE
::Storage
::storage_check_enabled
($storecfg, $storage);
3698 # check if storage is available on target node
3699 PVE
::Storage
::storage_check_enabled
($storecfg, $storage, $target);
3700 # clone only works if target storage is shared
3701 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storage);
3702 die "can't clone to non-shared storage '$storage'\n"
3703 if !$scfg->{shared
};
3707 PVE
::Cluster
::check_cfs_quorum
();
3709 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3710 PVE
::QemuConfig-
>check_lock($conf);
3712 my $verify_running = PVE
::QemuServer
::check_running
($vmid) || 0;
3713 die "unexpected state change\n" if $verify_running != $running;
3715 die "snapshot '$snapname' does not exist\n"
3716 if $snapname && !defined( $conf->{snapshots
}->{$snapname});
3718 my $full = $param->{full
} // !PVE
::QemuConfig-
>is_template($conf);
3720 die "parameter 'storage' not allowed for linked clones\n"
3721 if defined($storage) && !$full;
3723 die "parameter 'format' not allowed for linked clones\n"
3724 if defined($format) && !$full;
3726 my $oldconf = $snapname ?
$conf->{snapshots
}->{$snapname} : $conf;
3728 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
3729 PVE
::QemuServer
::check_mapping_access
($rpcenv, $authuser, $oldconf);
3731 PVE
::QemuServer
::check_bridge_access
($rpcenv, $authuser, $oldconf);
3733 die "can't clone VM to node '$target' (VM uses local storage)\n"
3734 if $target && !$sharedvm;
3736 my $conffile = PVE
::QemuConfig-
>config_file($newid);
3737 die "unable to create VM $newid: config file already exists\n"
3740 my $newconf = { lock => 'clone' };
3745 foreach my $opt (keys %$oldconf) {
3746 my $value = $oldconf->{$opt};
3748 # do not copy snapshot related info
3749 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
3750 $opt eq 'vmstate' || $opt eq 'snapstate';
3752 # no need to copy unused images, because VMID(owner) changes anyways
3753 next if $opt =~ m/^unused\d+$/;
3755 die "cannot clone TPM state while VM is running\n"
3756 if $full && $running && !$snapname && $opt eq 'tpmstate0';
3758 # always change MAC! address
3759 if ($opt =~ m/^net(\d+)$/) {
3760 my $net = PVE
::QemuServer
::parse_net
($value);
3761 my $dc = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
3762 $net->{macaddr
} = PVE
::Tools
::random_ether_addr
($dc->{mac_prefix
});
3763 $newconf->{$opt} = PVE
::QemuServer
::print_net
($net);
3764 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
3765 my $drive = PVE
::QemuServer
::parse_drive
($opt, $value);
3766 die "unable to parse drive options for '$opt'\n" if !$drive;
3767 if (PVE
::QemuServer
::drive_is_cdrom
($drive, 1)) {
3768 $newconf->{$opt} = $value; # simply copy configuration
3770 if ($full || PVE
::QemuServer
::drive_is_cloudinit
($drive)) {
3771 die "Full clone feature is not supported for drive '$opt'\n"
3772 if !PVE
::Storage
::volume_has_feature
($storecfg, 'copy', $drive->{file
}, $snapname, $running);
3773 $fullclone->{$opt} = 1;
3775 # not full means clone instead of copy
3776 die "Linked clone feature is not supported for drive '$opt'\n"
3777 if !PVE
::Storage
::volume_has_feature
($storecfg, 'clone', $drive->{file
}, $snapname, $running);
3779 $drives->{$opt} = $drive;
3780 next if PVE
::QemuServer
::drive_is_cloudinit
($drive);
3781 push @$vollist, $drive->{file
};
3784 # copy everything else
3785 $newconf->{$opt} = $value;
3789 return ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone);
3793 my ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone) = $load_and_check->();
3794 my $storecfg = PVE
::Storage
::config
();
3796 # auto generate a new uuid
3797 my $smbios1 = PVE
::QemuServer
::parse_smbios1
($newconf->{smbios1
} || '');
3798 $smbios1->{uuid
} = PVE
::QemuServer
::generate_uuid
();
3799 $newconf->{smbios1
} = PVE
::QemuServer
::print_smbios1
($smbios1);
3800 # auto generate a new vmgenid only if the option was set for template
3801 if ($newconf->{vmgenid
}) {
3802 $newconf->{vmgenid
} = PVE
::QemuServer
::generate_uuid
();
3805 delete $newconf->{template
};
3807 if ($param->{name
}) {
3808 $newconf->{name
} = $param->{name
};
3810 $newconf->{name
} = "Copy-of-VM-" . ($oldconf->{name
} // $vmid);
3813 if ($param->{description
}) {
3814 $newconf->{description
} = $param->{description
};
3817 # create empty/temp config - this fails if VM already exists on other node
3818 # FIXME use PVE::QemuConfig->create_and_lock_config and adapt code
3819 PVE
::Tools
::file_set_contents
($conffile, "# qmclone temporary file\nlock: clone\n");
3821 PVE
::Firewall
::clone_vmfw_conf
($vmid, $newid);
3823 my $newvollist = [];
3830 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
3832 PVE
::Storage
::activate_volumes
($storecfg, $vollist, $snapname);
3834 my $bwlimit = extract_param
($param, 'bwlimit');
3836 my $total_jobs = scalar(keys %{$drives});
3839 foreach my $opt (sort keys %$drives) {
3840 my $drive = $drives->{$opt};
3841 my $skipcomplete = ($total_jobs != $i); # finish after last drive
3842 my $completion = $skipcomplete ?
'skip' : 'complete';
3844 my $src_sid = PVE
::Storage
::parse_volume_id
($drive->{file
});
3845 my $storage_list = [ $src_sid ];
3846 push @$storage_list, $storage if defined($storage);
3847 my $clonelimit = PVE
::Storage
::get_bandwidth_limit
('clone', $storage_list, $bwlimit);
3851 running
=> $running,
3854 snapname
=> $snapname,
3860 storage
=> $storage,
3864 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($oldconf)
3865 if $opt eq 'efidisk0';
3867 my $newdrive = PVE
::QemuServer
::clone_disk
(
3879 $newconf->{$opt} = PVE
::QemuServer
::print_drive
($newdrive);
3881 PVE
::QemuConfig-
>write_config($newid, $newconf);
3885 delete $newconf->{lock};
3887 # do not write pending changes
3888 if (my @changes = keys %{$newconf->{pending
}}) {
3889 my $pending = join(',', @changes);
3890 warn "found pending changes for '$pending', discarding for clone\n";
3891 delete $newconf->{pending
};
3894 PVE
::QemuConfig-
>write_config($newid, $newconf);
3896 PVE
::QemuServer
::create_ifaces_ipams_ips
($newconf, $newid);
3900 # always deactivate volumes – avoids that LVM LVs are active on several nodes
3901 eval { PVE
::Storage
::deactivate_volumes
($storecfg, $vollist, $snapname) };
3902 # but only warn when that fails (e.g., parallel clones keeping them active)
3906 PVE
::Storage
::deactivate_volumes
($storecfg, $newvollist);
3908 my $newconffile = PVE
::QemuConfig-
>config_file($newid, $target);
3909 die "Failed to move config to node '$target' - rename failed: $!\n"
3910 if !rename($conffile, $newconffile);
3913 PVE
::AccessControl
::add_vm_to_pool
($newid, $pool) if $pool;
3916 eval { PVE
::QemuServer
::qemu_blockjobs_cancel
($vmid, $jobs) };
3917 sleep 1; # some storage like rbd need to wait before release volume - really?
3919 foreach my $volid (@$newvollist) {
3920 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
3924 PVE
::Firewall
::remove_vmfw_conf
($newid);
3926 unlink $conffile; # avoid races -> last thing before die
3928 die "clone failed: $err";
3934 # Aquire exclusive lock lock for $newid
3935 my $lock_target_vm = sub {
3936 return PVE
::QemuConfig-
>lock_config_full($newid, 1, $clonefn);
3939 my $lock_source_vm = sub {
3940 # exclusive lock if VM is running - else shared lock is enough;
3942 return PVE
::QemuConfig-
>lock_config_full($vmid, 1, $lock_target_vm);
3944 return PVE
::QemuConfig-
>lock_config_shared($vmid, 1, $lock_target_vm);
3948 $load_and_check->(); # early checks before forking/locking
3950 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $lock_source_vm);
3953 __PACKAGE__-
>register_method({
3954 name
=> 'move_vm_disk',
3955 path
=> '{vmid}/move_disk',
3959 description
=> "Move volume to different storage or to a different VM.",
3961 description
=> "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
3962 "and 'Datastore.AllocateSpace' permissions on the storage. To move ".
3963 "a disk to another VM, you need the permissions on the target VM as well.",
3964 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
3967 additionalProperties
=> 0,
3969 node
=> get_standard_option
('pve-node'),
3970 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3971 'target-vmid' => get_standard_option
('pve-vmid', {
3972 completion
=> \
&PVE
::QemuServer
::complete_vmid
,
3977 description
=> "The disk you want to move.",
3978 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
3980 storage
=> get_standard_option
('pve-storage-id', {
3981 description
=> "Target storage.",
3982 completion
=> \
&PVE
::QemuServer
::complete_storage
,
3987 description
=> "Target Format.",
3988 enum
=> [ 'raw', 'qcow2', 'vmdk' ],
3993 description
=> "Delete the original disk after successful copy. By default the"
3994 ." original disk is kept as unused disk.",
4000 description
=> 'Prevent changes if current configuration file has different SHA1"
4001 ." digest. This can be used to prevent concurrent modifications.',
4006 description
=> "Override I/O bandwidth limit (in KiB/s).",
4010 default => 'move limit from datacenter or storage config',
4014 description
=> "The config key the disk will be moved to on the target VM"
4015 ." (for example, ide0 or scsi1). Default is the source disk key.",
4016 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
4019 'target-digest' => {
4021 description
=> 'Prevent changes if the current config file of the target VM has a"
4022 ." different SHA1 digest. This can be used to detect concurrent modifications.',
4030 description
=> "the task ID.",
4035 my $rpcenv = PVE
::RPCEnvironment
::get
();
4036 my $authuser = $rpcenv->get_user();
4038 my $node = extract_param
($param, 'node');
4039 my $vmid = extract_param
($param, 'vmid');
4040 my $target_vmid = extract_param
($param, 'target-vmid');
4041 my $digest = extract_param
($param, 'digest');
4042 my $target_digest = extract_param
($param, 'target-digest');
4043 my $disk = extract_param
($param, 'disk');
4044 my $target_disk = extract_param
($param, 'target-disk') // $disk;
4045 my $storeid = extract_param
($param, 'storage');
4046 my $format = extract_param
($param, 'format');
4048 my $storecfg = PVE
::Storage
::config
();
4050 my $load_and_check_move = sub {
4051 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4052 PVE
::QemuConfig-
>check_lock($conf);
4054 PVE
::Tools
::assert_if_modified
($digest, $conf->{digest
});
4056 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4058 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
4060 die "disk '$disk' has no associated volume\n" if !$drive->{file
};
4061 die "you can't move a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
4063 my $old_volid = $drive->{file
};
4065 my ($oldstoreid, $oldvolname) = PVE
::Storage
::parse_volume_id
($old_volid);
4066 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
4070 die "you can't move to the same storage with same format\n"
4071 if $oldstoreid eq $storeid && (!$format || !$oldfmt || $oldfmt eq $format);
4073 # this only checks snapshots because $disk is passed!
4074 my $snapshotted = PVE
::QemuServer
::Drive
::is_volume_in_use
(
4080 die "you can't move a disk with snapshots and delete the source\n"
4081 if $snapshotted && $param->{delete};
4083 return ($conf, $drive, $oldstoreid, $snapshotted);
4086 my $move_updatefn = sub {
4087 my ($conf, $drive, $oldstoreid, $snapshotted) = $load_and_check_move->();
4088 my $old_volid = $drive->{file
};
4090 PVE
::Cluster
::log_msg
(
4093 "move disk VM $vmid: move --disk $disk --storage $storeid"
4096 my $running = PVE
::QemuServer
::check_running
($vmid);
4098 PVE
::Storage
::activate_volumes
($storecfg, [ $drive->{file
} ]);
4100 my $newvollist = [];
4106 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
4108 warn "moving disk with snapshots, snapshots will not be moved!\n"
4111 my $bwlimit = extract_param
($param, 'bwlimit');
4112 my $movelimit = PVE
::Storage
::get_bandwidth_limit
(
4114 [$oldstoreid, $storeid],
4120 running
=> $running,
4129 storage
=> $storeid,
4133 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($conf)
4134 if $disk eq 'efidisk0';
4136 my $newdrive = PVE
::QemuServer
::clone_disk
(
4147 $conf->{$disk} = PVE
::QemuServer
::print_drive
($newdrive);
4149 PVE
::QemuConfig-
>add_unused_volume($conf, $old_volid) if !$param->{delete};
4151 # convert moved disk to base if part of template
4152 PVE
::QemuServer
::template_create
($vmid, $conf, $disk)
4153 if PVE
::QemuConfig-
>is_template($conf);
4155 PVE
::QemuConfig-
>write_config($vmid, $conf);
4157 my $do_trim = PVE
::QemuServer
::get_qga_key
($conf, 'fstrim_cloned_disks');
4158 if ($running && $do_trim && PVE
::QemuServer
::qga_check_running
($vmid)) {
4159 eval { mon_cmd
($vmid, "guest-fstrim") };
4163 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
4164 PVE
::Storage
::deactivate_volumes
($storecfg, [ $newdrive->{file
} ])
4170 foreach my $volid (@$newvollist) {
4171 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid) };
4174 die "storage migration failed: $err";
4177 if ($param->{delete}) {
4179 PVE
::Storage
::deactivate_volumes
($storecfg, [$old_volid]);
4180 PVE
::Storage
::vdisk_free
($storecfg, $old_volid);
4186 my $load_and_check_reassign_configs = sub {
4187 my $vmlist = PVE
::Cluster
::get_vmlist
()->{ids
};
4189 die "could not find VM ${vmid}\n" if !exists($vmlist->{$vmid});
4190 die "could not find target VM ${target_vmid}\n" if !exists($vmlist->{$target_vmid});
4192 my $source_node = $vmlist->{$vmid}->{node
};
4193 my $target_node = $vmlist->{$target_vmid}->{node
};
4195 die "Both VMs need to be on the same node ($source_node != $target_node)\n"
4196 if $source_node ne $target_node;
4198 my $source_conf = PVE
::QemuConfig-
>load_config($vmid);
4199 PVE
::QemuConfig-
>check_lock($source_conf);
4200 my $target_conf = PVE
::QemuConfig-
>load_config($target_vmid);
4201 PVE
::QemuConfig-
>check_lock($target_conf);
4203 die "Can't move disks from or to template VMs\n"
4204 if ($source_conf->{template
} || $target_conf->{template
});
4207 eval { PVE
::Tools
::assert_if_modified
($digest, $source_conf->{digest
}) };
4208 die "VM ${vmid}: $@" if $@;
4211 if ($target_digest) {
4212 eval { PVE
::Tools
::assert_if_modified
($target_digest, $target_conf->{digest
}) };
4213 die "VM ${target_vmid}: $@" if $@;
4216 die "Disk '${disk}' for VM '$vmid' does not exist\n" if !defined($source_conf->{$disk});
4218 die "Target disk key '${target_disk}' is already in use for VM '$target_vmid'\n"
4219 if $target_conf->{$target_disk};
4221 my $drive = PVE
::QemuServer
::parse_drive
(
4223 $source_conf->{$disk},
4225 die "failed to parse source disk - $@\n" if !$drive;
4227 my $source_volid = $drive->{file
};
4229 die "disk '${disk}' has no associated volume\n" if !$source_volid;
4230 die "CD drive contents can't be moved to another VM\n"
4231 if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
4233 my $storeid = PVE
::Storage
::parse_volume_id
($source_volid, 1);
4234 die "Volume '$source_volid' not managed by PVE\n" if !defined($storeid);
4236 die "Can't move disk used by a snapshot to another VM\n"
4237 if PVE
::QemuServer
::Drive
::is_volume_in_use
($storecfg, $source_conf, $disk, $source_volid);
4238 die "Storage does not support moving of this disk to another VM\n"
4239 if (!PVE
::Storage
::volume_has_feature
($storecfg, 'rename', $source_volid));
4240 die "Cannot move disk to another VM while the source VM is running - detach first\n"
4241 if PVE
::QemuServer
::check_running
($vmid) && $disk !~ m/^unused\d+$/;
4243 # now re-parse using target disk slot format
4244 if ($target_disk =~ /^unused\d+$/) {
4245 $drive = PVE
::QemuServer
::parse_drive
(
4250 $drive = PVE
::QemuServer
::parse_drive
(
4252 $source_conf->{$disk},
4255 die "failed to parse source disk for target disk format - $@\n" if !$drive;
4257 my $repl_conf = PVE
::ReplicationConfig-
>new();
4258 if ($repl_conf->check_for_existing_jobs($target_vmid, 1)) {
4259 my $format = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4260 die "Cannot move disk to a replicated VM. Storage does not support replication!\n"
4261 if !PVE
::Storage
::storage_can_replicate
($storecfg, $storeid, $format);
4264 return ($source_conf, $target_conf, $drive);
4269 print STDERR
"$msg\n";
4272 my $disk_reassignfn = sub {
4273 return PVE
::QemuConfig-
>lock_config($vmid, sub {
4274 return PVE
::QemuConfig-
>lock_config($target_vmid, sub {
4275 my ($source_conf, $target_conf, $drive) = &$load_and_check_reassign_configs();
4277 my $source_volid = $drive->{file
};
4279 print "moving disk '$disk' from VM '$vmid' to '$target_vmid'\n";
4280 my ($storeid, $source_volname) = PVE
::Storage
::parse_volume_id
($source_volid);
4282 my $fmt = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4284 my $new_volid = PVE
::Storage
::rename_volume
(
4290 $drive->{file
} = $new_volid;
4292 my $boot_order = PVE
::QemuServer
::device_bootorder
($source_conf);
4293 if (defined(delete $boot_order->{$disk})) {
4294 print "removing disk '$disk' from boot order config\n";
4295 my $boot_devs = [ sort { $boot_order->{$a} <=> $boot_order->{$b} } keys %$boot_order ];
4296 $source_conf->{boot
} = PVE
::QemuServer
::print_bootorder
($boot_devs);
4299 delete $source_conf->{$disk};
4300 print "removing disk '${disk}' from VM '${vmid}' config\n";
4301 PVE
::QemuConfig-
>write_config($vmid, $source_conf);
4303 my $drive_string = PVE
::QemuServer
::print_drive
($drive);
4305 if ($target_disk =~ /^unused\d+$/) {
4306 $target_conf->{$target_disk} = $drive_string;
4307 PVE
::QemuConfig-
>write_config($target_vmid, $target_conf);
4312 vmid
=> $target_vmid,
4313 digest
=> $target_digest,
4314 $target_disk => $drive_string,
4320 # remove possible replication snapshots
4321 if (PVE
::Storage
::volume_has_feature
(
4327 PVE
::Replication
::prepare
(
4337 print "Failed to remove replication snapshots on moved disk " .
4338 "'$target_disk'. Manual cleanup could be necessary.\n";
4345 if ($target_vmid && $storeid) {
4346 my $msg = "either set 'storage' or 'target-vmid', but not both";
4347 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4348 } elsif ($target_vmid) {
4349 $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
4350 if $authuser ne 'root@pam';
4352 raise_param_exc
({ 'target-vmid' => "must be different than source VMID to reassign disk" })
4353 if $vmid eq $target_vmid;
4355 my (undef, undef, $drive) = &$load_and_check_reassign_configs();
4356 my $storage = PVE
::Storage
::parse_volume_id
($drive->{file
});
4357 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
4359 return $rpcenv->fork_worker(
4361 "${vmid}-${disk}>${target_vmid}-${target_disk}",
4365 } elsif ($storeid) {
4366 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4368 $load_and_check_move->(); # early checks before forking/locking
4371 PVE
::QemuConfig-
>lock_config($vmid, $move_updatefn);
4374 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
4376 my $msg = "both 'storage' and 'target-vmid' missing, either needs to be set";
4377 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4381 my $check_vm_disks_local = sub {
4382 my ($storecfg, $vmconf, $vmid) = @_;
4384 my $local_disks = {};
4386 # add some more information to the disks e.g. cdrom
4387 PVE
::QemuServer
::foreach_volid
($vmconf, sub {
4388 my ($volid, $attr) = @_;
4390 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
4392 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
4393 return if $scfg->{shared
};
4395 # The shared attr here is just a special case where the vdisk
4396 # is marked as shared manually
4397 return if $attr->{shared
};
4398 return if $attr->{cdrom
} and $volid eq "none";
4400 if (exists $local_disks->{$volid}) {
4401 @{$local_disks->{$volid}}{keys %$attr} = values %$attr
4403 $local_disks->{$volid} = $attr;
4404 # ensure volid is present in case it's needed
4405 $local_disks->{$volid}->{volid
} = $volid;
4409 return $local_disks;
4412 __PACKAGE__-
>register_method({
4413 name
=> 'migrate_vm_precondition',
4414 path
=> '{vmid}/migrate',
4418 description
=> "Get preconditions for migration.",
4420 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4423 additionalProperties
=> 0,
4425 node
=> get_standard_option
('pve-node'),
4426 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4427 target
=> get_standard_option
('pve-node', {
4428 description
=> "Target node.",
4429 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4437 running
=> { type
=> 'boolean' },
4441 description
=> "List nodes allowed for offline migration, only passed if VM is offline"
4443 not_allowed_nodes
=> {
4446 description
=> "List not allowed nodes with additional informations, only passed if VM is offline"
4450 description
=> "List local disks including CD-Rom, unsused and not referenced disks"
4452 local_resources
=> {
4454 description
=> "List local resources e.g. pci, usb"
4456 'mapped-resources' => {
4458 description
=> "List of mapped resources e.g. pci, usb"
4465 my $rpcenv = PVE
::RPCEnvironment
::get
();
4467 my $authuser = $rpcenv->get_user();
4469 PVE
::Cluster
::check_cfs_quorum
();
4473 my $vmid = extract_param
($param, 'vmid');
4474 my $target = extract_param
($param, 'target');
4475 my $localnode = PVE
::INotify
::nodename
();
4479 my $vmconf = PVE
::QemuConfig-
>load_config($vmid);
4480 my $storecfg = PVE
::Storage
::config
();
4483 # try to detect errors early
4484 PVE
::QemuConfig-
>check_lock($vmconf);
4486 $res->{running
} = PVE
::QemuServer
::check_running
($vmid) ?
1:0;
4488 my ($local_resources, $mapped_resources, $missing_mappings_by_node) =
4489 PVE
::QemuServer
::check_local_resources
($vmconf, 1);
4490 delete $missing_mappings_by_node->{$localnode};
4492 my $vga = PVE
::QemuServer
::parse_vga
($vmconf->{vga
});
4493 if ($res->{running
} && $vga->{'clipboard'} && $vga->{'clipboard'} eq 'vnc') {
4494 push $local_resources->@*, "clipboard=vnc";
4497 # if vm is not running, return target nodes where local storage/mapped devices are available
4498 # for offline migration
4499 if (!$res->{running
}) {
4500 $res->{allowed_nodes
} = [];
4501 my $checked_nodes = PVE
::QemuServer
::check_local_storage_availability
($vmconf, $storecfg);
4502 delete $checked_nodes->{$localnode};
4504 foreach my $node (keys %$checked_nodes) {
4505 my $missing_mappings = $missing_mappings_by_node->{$node};
4506 if (scalar($missing_mappings->@*)) {
4507 $checked_nodes->{$node}->{'unavailable-resources'} = $missing_mappings;
4511 if (!defined($checked_nodes->{$node}->{unavailable_storages
})) {
4512 push @{$res->{allowed_nodes
}}, $node;
4516 $res->{not_allowed_nodes
} = $checked_nodes;
4519 my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
4520 $res->{local_disks
} = [ values %$local_disks ];;
4522 $res->{local_resources
} = $local_resources;
4523 $res->{'mapped-resources'} = $mapped_resources;
4530 __PACKAGE__-
>register_method({
4531 name
=> 'migrate_vm',
4532 path
=> '{vmid}/migrate',
4536 description
=> "Migrate virtual machine. Creates a new migration task.",
4538 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4541 additionalProperties
=> 0,
4543 node
=> get_standard_option
('pve-node'),
4544 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4545 target
=> get_standard_option
('pve-node', {
4546 description
=> "Target node.",
4547 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4551 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4556 description
=> "Allow to migrate VMs which use local devices. Only root may use this option.",
4561 enum
=> ['secure', 'insecure'],
4562 description
=> "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
4565 migration_network
=> {
4566 type
=> 'string', format
=> 'CIDR',
4567 description
=> "CIDR of the (sub) network that is used for migration.",
4570 "with-local-disks" => {
4572 description
=> "Enable live storage migration for local disk",
4575 targetstorage
=> get_standard_option
('pve-targetstorage', {
4576 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4579 description
=> "Override I/O bandwidth limit (in KiB/s).",
4583 default => 'migrate limit from datacenter or storage config',
4589 description
=> "the task ID.",
4594 my $rpcenv = PVE
::RPCEnvironment
::get
();
4595 my $authuser = $rpcenv->get_user();
4597 my $target = extract_param
($param, 'target');
4599 my $localnode = PVE
::INotify
::nodename
();
4600 raise_param_exc
({ target
=> "target is local node."}) if $target eq $localnode;
4602 PVE
::Cluster
::check_cfs_quorum
();
4604 PVE
::Cluster
::check_node_exists
($target);
4606 my $targetip = PVE
::Cluster
::remote_node_ip
($target);
4608 my $vmid = extract_param
($param, 'vmid');
4610 raise_param_exc
({ force
=> "Only root may use this option." })
4611 if $param->{force
} && $authuser ne 'root@pam';
4613 raise_param_exc
({ migration_type
=> "Only root may use this option." })
4614 if $param->{migration_type
} && $authuser ne 'root@pam';
4616 # allow root only until better network permissions are available
4617 raise_param_exc
({ migration_network
=> "Only root may use this option." })
4618 if $param->{migration_network
} && $authuser ne 'root@pam';
4621 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4623 # try to detect errors early
4625 PVE
::QemuConfig-
>check_lock($conf);
4627 if (PVE
::QemuServer
::check_running
($vmid)) {
4628 die "can't migrate running VM without --online\n" if !$param->{online
};
4630 my $repl_conf = PVE
::ReplicationConfig-
>new();
4631 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
4632 my $is_replicated_to_target = defined($repl_conf->find_local_replication_job($vmid, $target));
4633 if (!$param->{force
} && $is_replicated && !$is_replicated_to_target) {
4634 die "Cannot live-migrate replicated VM to node '$target' - not a replication " .
4635 "target. Use 'force' to override.\n";
4638 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4639 $param->{online
} = 0;
4642 my $storecfg = PVE
::Storage
::config
();
4643 if (my $targetstorage = $param->{targetstorage
}) {
4644 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($targetstorage, 'pve-storage-id') };
4645 raise_param_exc
({ targetstorage
=> "failed to parse storage map: $@" })
4648 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk'])
4649 if !defined($storagemap->{identity
});
4651 foreach my $target_sid (values %{$storagemap->{entries
}}) {
4652 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $target_sid, $target);
4655 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storagemap->{default}, $target)
4656 if $storagemap->{default};
4658 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target)
4659 if $storagemap->{identity
};
4661 $param->{storagemap
} = $storagemap;
4663 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target);
4666 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
4671 print "Requesting HA migration for VM $vmid to node $target\n";
4673 my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
4674 PVE
::Tools
::run_command
($cmd);
4678 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
4683 PVE
::QemuMigrate-
>migrate($target, $targetip, $vmid, $param);
4687 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
4690 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
4695 __PACKAGE__-
>register_method({
4696 name
=> 'remote_migrate_vm',
4697 path
=> '{vmid}/remote_migrate',
4701 description
=> "Migrate virtual machine to a remote cluster. Creates a new migration task. EXPERIMENTAL feature!",
4703 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4706 additionalProperties
=> 0,
4708 node
=> get_standard_option
('pve-node'),
4709 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4710 'target-vmid' => get_standard_option
('pve-vmid', { optional
=> 1 }),
4711 'target-endpoint' => get_standard_option
('proxmox-remote', {
4712 description
=> "Remote target endpoint",
4716 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4721 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.",
4725 'target-storage' => get_standard_option
('pve-targetstorage', {
4726 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4729 'target-bridge' => {
4731 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.",
4732 format
=> 'bridge-pair-list',
4735 description
=> "Override I/O bandwidth limit (in KiB/s).",
4739 default => 'migrate limit from datacenter or storage config',
4745 description
=> "the task ID.",
4750 my $rpcenv = PVE
::RPCEnvironment
::get
();
4751 my $authuser = $rpcenv->get_user();
4753 my $source_vmid = extract_param
($param, 'vmid');
4754 my $target_endpoint = extract_param
($param, 'target-endpoint');
4755 my $target_vmid = extract_param
($param, 'target-vmid') // $source_vmid;
4757 my $delete = extract_param
($param, 'delete') // 0;
4759 PVE
::Cluster
::check_cfs_quorum
();
4762 my $conf = PVE
::QemuConfig-
>load_config($source_vmid);
4764 PVE
::QemuConfig-
>check_lock($conf);
4766 raise_param_exc
({ vmid
=> "cannot migrate HA-managed VM to remote cluster" })
4767 if PVE
::HA
::Config
::vm_is_ha_managed
($source_vmid);
4769 my $remote = PVE
::JSONSchema
::parse_property_string
('proxmox-remote', $target_endpoint);
4771 # TODO: move this as helper somewhere appropriate?
4773 protocol
=> 'https',
4774 host
=> $remote->{host
},
4775 port
=> $remote->{port
} // 8006,
4776 apitoken
=> $remote->{apitoken
},
4780 if ($fp = $remote->{fingerprint
}) {
4781 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 };
4784 print "Establishing API connection with remote at '$remote->{host}'\n";
4786 my $api_client = PVE
::APIClient
::LWP-
>new(%$conn_args);
4788 if (!defined($fp)) {
4789 my $cert_info = $api_client->get("/nodes/localhost/certificates/info");
4790 foreach my $cert (@$cert_info) {
4791 my $filename = $cert->{filename
};
4792 next if $filename ne 'pveproxy-ssl.pem' && $filename ne 'pve-ssl.pem';
4793 $fp = $cert->{fingerprint
} if !$fp || $filename eq 'pveproxy-ssl.pem';
4795 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 }
4799 my $repl_conf = PVE
::ReplicationConfig-
>new();
4800 my $is_replicated = $repl_conf->check_for_existing_jobs($source_vmid, 1);
4801 die "cannot remote-migrate replicated VM\n" if $is_replicated;
4803 if (PVE
::QemuServer
::check_running
($source_vmid)) {
4804 die "can't migrate running VM without --online\n" if !$param->{online
};
4807 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4808 $param->{online
} = 0;
4811 my $storecfg = PVE
::Storage
::config
();
4812 my $target_storage = extract_param
($param, 'target-storage');
4813 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($target_storage, 'pve-storage-id') };
4814 raise_param_exc
({ 'target-storage' => "failed to parse storage map: $@" })
4817 my $target_bridge = extract_param
($param, 'target-bridge');
4818 my $bridgemap = eval { PVE
::JSONSchema
::parse_idmap
($target_bridge, 'pve-bridge-id') };
4819 raise_param_exc
({ 'target-bridge' => "failed to parse bridge map: $@" })
4822 die "remote migration requires explicit storage mapping!\n"
4823 if $storagemap->{identity
};
4825 $param->{storagemap
} = $storagemap;
4826 $param->{bridgemap
} = $bridgemap;
4827 $param->{remote
} = {
4828 conn
=> $conn_args, # re-use fingerprint for tunnel
4829 client
=> $api_client,
4830 vmid
=> $target_vmid,
4832 $param->{migration_type
} = 'websocket';
4833 $param->{'with-local-disks'} = 1;
4834 $param->{delete} = $delete if $delete;
4836 my $cluster_status = $api_client->get("/cluster/status");
4838 foreach my $entry (@$cluster_status) {
4839 next if $entry->{type
} ne 'node';
4840 if ($entry->{local}) {
4841 $target_node = $entry->{name
};
4846 die "couldn't determine endpoint's node name\n"
4847 if !defined($target_node);
4850 PVE
::QemuMigrate-
>migrate($target_node, $remote->{host
}, $source_vmid, $param);
4854 return PVE
::GuestHelpers
::guest_migration_lock
($source_vmid, 10, $realcmd);
4857 return $rpcenv->fork_worker('qmigrate', $source_vmid, $authuser, $worker);
4860 __PACKAGE__-
>register_method({
4862 path
=> '{vmid}/monitor',
4866 description
=> "Execute QEMU monitor commands.",
4868 description
=> "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
4869 check
=> ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
4872 additionalProperties
=> 0,
4874 node
=> get_standard_option
('pve-node'),
4875 vmid
=> get_standard_option
('pve-vmid'),
4878 description
=> "The monitor command.",
4882 returns
=> { type
=> 'string'},
4886 my $rpcenv = PVE
::RPCEnvironment
::get
();
4887 my $authuser = $rpcenv->get_user();
4890 my $command = shift;
4891 return $command =~ m/^\s*info(\s+|$)/
4892 || $command =~ m/^\s*help\s*$/;
4895 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
4896 if !&$is_ro($param->{command
});
4898 my $vmid = $param->{vmid
};
4900 my $conf = PVE
::QemuConfig-
>load_config ($vmid); # check if VM exists
4904 $res = PVE
::QemuServer
::Monitor
::hmp_cmd
($vmid, $param->{command
});
4906 $res = "ERROR: $@" if $@;
4911 __PACKAGE__-
>register_method({
4912 name
=> 'resize_vm',
4913 path
=> '{vmid}/resize',
4917 description
=> "Extend volume size.",
4919 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
4922 additionalProperties
=> 0,
4924 node
=> get_standard_option
('pve-node'),
4925 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4926 skiplock
=> get_standard_option
('skiplock'),
4929 description
=> "The disk you want to resize.",
4930 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
4934 pattern
=> '\+?\d+(\.\d+)?[KMGT]?',
4935 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.",
4939 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
4947 description
=> "the task ID.",
4952 my $rpcenv = PVE
::RPCEnvironment
::get
();
4954 my $authuser = $rpcenv->get_user();
4956 my $node = extract_param
($param, 'node');
4958 my $vmid = extract_param
($param, 'vmid');
4960 my $digest = extract_param
($param, 'digest');
4962 my $disk = extract_param
($param, 'disk');
4964 my $sizestr = extract_param
($param, 'size');
4966 my $skiplock = extract_param
($param, 'skiplock');
4967 raise_param_exc
({ skiplock
=> "Only root may use this option." })
4968 if $skiplock && $authuser ne 'root@pam';
4970 my $storecfg = PVE
::Storage
::config
();
4972 my $updatefn = sub {
4974 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4976 die "checksum missmatch (file change by other user?)\n"
4977 if $digest && $digest ne $conf->{digest
};
4978 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
4980 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4982 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
4984 my (undef, undef, undef, undef, undef, undef, $format) =
4985 PVE
::Storage
::parse_volname
($storecfg, $drive->{file
});
4987 my $volid = $drive->{file
};
4989 die "disk '$disk' has no associated volume\n" if !$volid;
4991 die "you can't resize a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive);
4993 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
4995 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4997 PVE
::Storage
::activate_volumes
($storecfg, [$volid]);
4998 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid, 5);
5000 die "Could not determine current size of volume '$volid'\n" if !defined($size);
5002 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
5003 my ($ext, $newsize, $unit) = ($1, $2, $4);
5006 $newsize = $newsize * 1024;
5007 } elsif ($unit eq 'M') {
5008 $newsize = $newsize * 1024 * 1024;
5009 } elsif ($unit eq 'G') {
5010 $newsize = $newsize * 1024 * 1024 * 1024;
5011 } elsif ($unit eq 'T') {
5012 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
5015 $newsize += $size if $ext;
5016 $newsize = int($newsize);
5018 die "shrinking disks is not supported\n" if $newsize < $size;
5020 return if $size == $newsize;
5022 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
5024 PVE
::QemuServer
::qemu_block_resize
($vmid, "drive-$disk", $storecfg, $volid, $newsize);
5026 $drive->{size
} = $newsize;
5027 $conf->{$disk} = PVE
::QemuServer
::print_drive
($drive);
5029 PVE
::QemuConfig-
>write_config($vmid, $conf);
5033 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
5036 return $rpcenv->fork_worker('resize', $vmid, $authuser, $worker);
5039 __PACKAGE__-
>register_method({
5040 name
=> 'snapshot_list',
5041 path
=> '{vmid}/snapshot',
5043 description
=> "List all snapshots.",
5045 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5048 protected
=> 1, # qemu pid files are only readable by root
5050 additionalProperties
=> 0,
5052 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5053 node
=> get_standard_option
('pve-node'),
5062 description
=> "Snapshot identifier. Value 'current' identifies the current VM.",
5066 description
=> "Snapshot includes RAM.",
5071 description
=> "Snapshot description.",
5075 description
=> "Snapshot creation time",
5077 renderer
=> 'timestamp',
5081 description
=> "Parent snapshot identifier.",
5087 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
5092 my $vmid = $param->{vmid
};
5094 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5095 my $snaphash = $conf->{snapshots
} || {};
5099 foreach my $name (keys %$snaphash) {
5100 my $d = $snaphash->{$name};
5103 snaptime
=> $d->{snaptime
} || 0,
5104 vmstate
=> $d->{vmstate
} ?
1 : 0,
5105 description
=> $d->{description
} || '',
5107 $item->{parent
} = $d->{parent
} if $d->{parent
};
5108 $item->{snapstate
} = $d->{snapstate
} if $d->{snapstate
};
5112 my $running = PVE
::QemuServer
::check_running
($vmid, 1) ?
1 : 0;
5115 digest
=> $conf->{digest
},
5116 running
=> $running,
5117 description
=> "You are here!",
5119 $current->{parent
} = $conf->{parent
} if $conf->{parent
};
5121 push @$res, $current;
5126 __PACKAGE__-
>register_method({
5128 path
=> '{vmid}/snapshot',
5132 description
=> "Snapshot a VM.",
5134 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5137 additionalProperties
=> 0,
5139 node
=> get_standard_option
('pve-node'),
5140 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5141 snapname
=> get_standard_option
('pve-snapshot-name'),
5145 description
=> "Save the vmstate",
5150 description
=> "A textual description or comment.",
5156 description
=> "the task ID.",
5161 my $rpcenv = PVE
::RPCEnvironment
::get
();
5163 my $authuser = $rpcenv->get_user();
5165 my $node = extract_param
($param, 'node');
5167 my $vmid = extract_param
($param, 'vmid');
5169 my $snapname = extract_param
($param, 'snapname');
5171 die "unable to use snapshot name 'current' (reserved name)\n"
5172 if $snapname eq 'current';
5174 die "unable to use snapshot name 'pending' (reserved name)\n"
5175 if lc($snapname) eq 'pending';
5178 PVE
::Cluster
::log_msg
('info', $authuser, "snapshot VM $vmid: $snapname");
5179 PVE
::QemuConfig-
>snapshot_create($vmid, $snapname, $param->{vmstate
},
5180 $param->{description
});
5183 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
5186 __PACKAGE__-
>register_method({
5187 name
=> 'snapshot_cmd_idx',
5188 path
=> '{vmid}/snapshot/{snapname}',
5195 additionalProperties
=> 0,
5197 vmid
=> get_standard_option
('pve-vmid'),
5198 node
=> get_standard_option
('pve-node'),
5199 snapname
=> get_standard_option
('pve-snapshot-name'),
5208 links
=> [ { rel
=> 'child', href
=> "{cmd}" } ],
5215 push @$res, { cmd
=> 'rollback' };
5216 push @$res, { cmd
=> 'config' };
5221 __PACKAGE__-
>register_method({
5222 name
=> 'update_snapshot_config',
5223 path
=> '{vmid}/snapshot/{snapname}/config',
5227 description
=> "Update snapshot metadata.",
5229 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5232 additionalProperties
=> 0,
5234 node
=> get_standard_option
('pve-node'),
5235 vmid
=> get_standard_option
('pve-vmid'),
5236 snapname
=> get_standard_option
('pve-snapshot-name'),
5240 description
=> "A textual description or comment.",
5244 returns
=> { type
=> 'null' },
5248 my $rpcenv = PVE
::RPCEnvironment
::get
();
5250 my $authuser = $rpcenv->get_user();
5252 my $vmid = extract_param
($param, 'vmid');
5254 my $snapname = extract_param
($param, 'snapname');
5256 return if !defined($param->{description
});
5258 my $updatefn = sub {
5260 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5262 PVE
::QemuConfig-
>check_lock($conf);
5264 my $snap = $conf->{snapshots
}->{$snapname};
5266 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5268 $snap->{description
} = $param->{description
} if defined($param->{description
});
5270 PVE
::QemuConfig-
>write_config($vmid, $conf);
5273 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
5278 __PACKAGE__-
>register_method({
5279 name
=> 'get_snapshot_config',
5280 path
=> '{vmid}/snapshot/{snapname}/config',
5283 description
=> "Get snapshot configuration",
5285 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback', 'VM.Audit' ], any
=> 1],
5288 additionalProperties
=> 0,
5290 node
=> get_standard_option
('pve-node'),
5291 vmid
=> get_standard_option
('pve-vmid'),
5292 snapname
=> get_standard_option
('pve-snapshot-name'),
5295 returns
=> { type
=> "object" },
5299 my $rpcenv = PVE
::RPCEnvironment
::get
();
5301 my $authuser = $rpcenv->get_user();
5303 my $vmid = extract_param
($param, 'vmid');
5305 my $snapname = extract_param
($param, 'snapname');
5307 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5309 my $snap = $conf->{snapshots
}->{$snapname};
5311 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5316 __PACKAGE__-
>register_method({
5318 path
=> '{vmid}/snapshot/{snapname}/rollback',
5322 description
=> "Rollback VM state to specified snapshot.",
5324 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any
=> 1],
5327 additionalProperties
=> 0,
5329 node
=> get_standard_option
('pve-node'),
5330 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5331 snapname
=> get_standard_option
('pve-snapshot-name'),
5334 description
=> "Whether the VM should get started after rolling back successfully."
5335 . " (Note: VMs will be automatically started if the snapshot includes RAM.)",
5343 description
=> "the task ID.",
5348 my $rpcenv = PVE
::RPCEnvironment
::get
();
5350 my $authuser = $rpcenv->get_user();
5352 my $node = extract_param
($param, 'node');
5354 my $vmid = extract_param
($param, 'vmid');
5356 my $snapname = extract_param
($param, 'snapname');
5359 PVE
::Cluster
::log_msg
('info', $authuser, "rollback snapshot VM $vmid: $snapname");
5360 PVE
::QemuConfig-
>snapshot_rollback($vmid, $snapname);
5362 if ($param->{start
} && !PVE
::QemuServer
::Helpers
::vm_running_locally
($vmid)) {
5363 PVE
::API2
::Qemu-
>vm_start({ vmid
=> $vmid, node
=> $node });
5368 # hold migration lock, this makes sure that nobody create replication snapshots
5369 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
5372 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
5375 __PACKAGE__-
>register_method({
5376 name
=> 'delsnapshot',
5377 path
=> '{vmid}/snapshot/{snapname}',
5381 description
=> "Delete a VM snapshot.",
5383 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5386 additionalProperties
=> 0,
5388 node
=> get_standard_option
('pve-node'),
5389 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5390 snapname
=> get_standard_option
('pve-snapshot-name'),
5394 description
=> "For removal from config file, even if removing disk snapshots fails.",
5400 description
=> "the task ID.",
5405 my $rpcenv = PVE
::RPCEnvironment
::get
();
5407 my $authuser = $rpcenv->get_user();
5409 my $node = extract_param
($param, 'node');
5411 my $vmid = extract_param
($param, 'vmid');
5413 my $snapname = extract_param
($param, 'snapname');
5416 my $do_delete = sub {
5418 PVE
::Cluster
::log_msg
('info', $authuser, "delete snapshot VM $vmid: $snapname");
5419 PVE
::QemuConfig-
>snapshot_delete($vmid, $snapname, $param->{force
});
5423 if ($param->{force
}) {
5426 eval { PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $do_delete); };
5428 die $err if $lock_obtained;
5429 die "Failed to obtain guest migration lock - replication running?\n";
5434 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
5437 __PACKAGE__-
>register_method({
5439 path
=> '{vmid}/template',
5443 description
=> "Create a Template.",
5445 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid}",
5446 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
5449 additionalProperties
=> 0,
5451 node
=> get_standard_option
('pve-node'),
5452 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
5456 description
=> "If you want to convert only 1 disk to base image.",
5457 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
5464 description
=> "the task ID.",
5469 my $rpcenv = PVE
::RPCEnvironment
::get
();
5471 my $authuser = $rpcenv->get_user();
5473 my $node = extract_param
($param, 'node');
5475 my $vmid = extract_param
($param, 'vmid');
5477 my $disk = extract_param
($param, 'disk');
5479 my $load_and_check = sub {
5480 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5482 PVE
::QemuConfig-
>check_lock($conf);
5484 die "unable to create template, because VM contains snapshots\n"
5485 if $conf->{snapshots
} && scalar(keys %{$conf->{snapshots
}});
5487 die "you can't convert a template to a template\n"
5488 if PVE
::QemuConfig-
>is_template($conf) && !$disk;
5490 die "you can't convert a VM to template if VM is running\n"
5491 if PVE
::QemuServer
::check_running
($vmid);
5496 $load_and_check->();
5499 PVE
::QemuConfig-
>lock_config($vmid, sub {
5500 my $conf = $load_and_check->();
5502 $conf->{template
} = 1;
5503 PVE
::QemuConfig-
>write_config($vmid, $conf);
5505 PVE
::QemuServer
::template_create
($vmid, $conf, $disk);
5509 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
5512 __PACKAGE__-
>register_method({
5513 name
=> 'cloudinit_generated_config_dump',
5514 path
=> '{vmid}/cloudinit/dump',
5517 description
=> "Get automatically generated cloudinit config.",
5519 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5522 additionalProperties
=> 0,
5524 node
=> get_standard_option
('pve-node'),
5525 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5527 description
=> 'Config type.',
5529 enum
=> ['user', 'network', 'meta'],
5539 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
5541 return PVE
::QemuServer
::Cloudinit
::dump_cloudinit_config
($conf, $param->{vmid
}, $param->{type
});
5544 __PACKAGE__-
>register_method({
5546 path
=> '{vmid}/mtunnel',
5549 description
=> 'Migration tunnel endpoint - only for internal use by VM migration.',
5553 ['perm', '/vms/{vmid}', [ 'VM.Allocate' ]],
5554 ['perm', '/', [ 'Sys.Incoming' ]],
5556 description
=> "You need 'VM.Allocate' permissions on '/vms/{vmid}' and Sys.Incoming" .
5557 " on '/'. Further permission checks happen during the actual migration.",
5560 additionalProperties
=> 0,
5562 node
=> get_standard_option
('pve-node'),
5563 vmid
=> get_standard_option
('pve-vmid'),
5566 format
=> 'pve-storage-id-list',
5568 description
=> 'List of storages to check permission and availability. Will be checked again for all actually used storages during migration.',
5572 format
=> 'pve-bridge-id-list',
5574 description
=> 'List of network bridges to check availability. Will be checked again for actually used bridges during migration.',
5579 additionalProperties
=> 0,
5581 upid
=> { type
=> 'string' },
5582 ticket
=> { type
=> 'string' },
5583 socket => { type
=> 'string' },
5589 my $rpcenv = PVE
::RPCEnvironment
::get
();
5590 my $authuser = $rpcenv->get_user();
5592 my $node = extract_param
($param, 'node');
5593 my $vmid = extract_param
($param, 'vmid');
5595 my $storages = extract_param
($param, 'storages');
5596 my $bridges = extract_param
($param, 'bridges');
5598 my $nodename = PVE
::INotify
::nodename
();
5600 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
5601 if $node ne 'localhost' && $node ne $nodename;
5605 my $storecfg = PVE
::Storage
::config
();
5606 foreach my $storeid (PVE
::Tools
::split_list
($storages)) {
5607 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storeid, $node);
5610 foreach my $bridge (PVE
::Tools
::split_list
($bridges)) {
5611 PVE
::Network
::read_bridge_mtu
($bridge);
5614 PVE
::Cluster
::check_cfs_quorum
();
5616 my $lock = 'create';
5617 eval { PVE
::QemuConfig-
>create_and_lock_config($vmid, 0, $lock); };
5619 raise_param_exc
({ vmid
=> "unable to create empty VM config - $@"})
5624 storecfg
=> PVE
::Storage
::config
(),
5629 my $run_locked = sub {
5630 my ($code, $params) = @_;
5631 return PVE
::QemuConfig-
>lock_config($state->{vmid
}, sub {
5632 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5634 $state->{conf
} = $conf;
5636 die "Encountered wrong lock - aborting mtunnel command handling.\n"
5637 if $state->{lock} && !PVE
::QemuConfig-
>has_lock($conf, $state->{lock});
5639 return $code->($params);
5647 description
=> 'Full VM config, adapted for target cluster/node',
5649 'firewall-config' => {
5651 description
=> 'VM firewall config',
5656 format
=> PVE
::JSONSchema
::get_standard_option
('pve-qm-image-format'),
5659 format
=> 'pve-storage-id',
5663 description
=> 'parsed drive information without volid and format',
5669 description
=> 'params passed to vm_start_nolock',
5673 description
=> 'migrate_opts passed to vm_start_nolock',
5679 description
=> 'socket path for which the ticket should be valid. must be known to current mtunnel instance.',
5685 description
=> 'remove VM config and disks, aborting migration',
5689 'disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'disk-import'},
5690 'query-disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'query-disk-import'},
5691 bwlimit
=> $PVE::StorageTunnel
::cmd_schema-
>{bwlimit
},
5694 my $cmd_handlers = {
5696 # compared against other end's version
5697 # bump/reset for breaking changes
5698 # bump/bump for opt-in changes
5700 api
=> $PVE::QemuMigrate
::WS_TUNNEL_VERSION
,
5707 # parse and write out VM FW config if given
5708 if (my $fw_conf = $params->{'firewall-config'}) {
5709 my ($path, $fh) = PVE
::Tools
::tempfile_contents
($fw_conf, 700);
5716 ipset_comments
=> {},
5718 my $cluster_fw_conf = PVE
::Firewall
::load_clusterfw_conf
();
5720 # TODO: add flag for strict parsing?
5721 # TODO: add import sub that does all this given raw content?
5722 my $vmfw_conf = PVE
::Firewall
::generic_fw_config_parser
($path, $cluster_fw_conf, $empty_conf, 'vm');
5723 $vmfw_conf->{vmid
} = $state->{vmid
};
5724 PVE
::Firewall
::save_vmfw_conf
($state->{vmid
}, $vmfw_conf);
5726 $state->{cleanup
}->{fw
} = 1;
5729 my $conf_fn = "incoming/qemu-server/$state->{vmid}.conf";
5730 my $new_conf = PVE
::QemuServer
::parse_vm_config
($conf_fn, $params->{conf
}, 1);
5731 delete $new_conf->{lock};
5732 delete $new_conf->{digest
};
5734 # TODO handle properly?
5735 delete $new_conf->{snapshots
};
5736 delete $new_conf->{parent
};
5737 delete $new_conf->{pending
};
5739 # not handled by update_vm_api
5740 my $vmgenid = delete $new_conf->{vmgenid
};
5741 my $meta = delete $new_conf->{meta
};
5742 my $cloudinit = delete $new_conf->{cloudinit
}; # this is informational only
5743 $new_conf->{skip_cloud_init
} = 1; # re-use image from source side
5745 $new_conf->{vmid
} = $state->{vmid
};
5746 $new_conf->{node
} = $node;
5748 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, 'create');
5751 $update_vm_api->($new_conf, 1);
5754 # revert to locked previous config
5755 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5756 $conf->{lock} = 'create';
5757 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5762 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5763 $conf->{lock} = 'migrate';
5764 $conf->{vmgenid
} = $vmgenid if defined($vmgenid);
5765 $conf->{meta
} = $meta if defined($meta);
5766 $conf->{cloudinit
} = $cloudinit if defined($cloudinit);
5767 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5769 $state->{lock} = 'migrate';
5775 return PVE
::StorageTunnel
::handle_bwlimit
($params);
5780 my $format = $params->{format
};
5781 my $storeid = $params->{storage
};
5782 my $drive = $params->{drive
};
5784 $check_storage_access_migrate->($rpcenv, $authuser, $state->{storecfg
}, $storeid, $node);
5787 default => $storeid,
5790 my $source_volumes = {
5800 my $res = PVE
::QemuServer
::vm_migrate_alloc_nbd_disks
($state->{storecfg
}, $state->{vmid
}, $source_volumes, $storagemap);
5801 if (defined($res->{disk
})) {
5802 $state->{cleanup
}->{volumes
}->{$res->{disk
}->{volid
}} = 1;
5803 return $res->{disk
};
5805 die "failed to allocate NBD disk..\n";
5808 'disk-import' => sub {
5811 $check_storage_access_migrate->(
5819 $params->{unix
} = "/run/qemu-server/$state->{vmid}.storage";
5821 return PVE
::StorageTunnel
::handle_disk_import
($state, $params);
5823 'query-disk-import' => sub {
5826 return PVE
::StorageTunnel
::handle_query_disk_import
($state, $params);
5831 my $info = PVE
::QemuServer
::vm_start_nolock
(
5835 $params->{start_params
},
5836 $params->{migrate_opts
},
5840 if ($info->{migrate
}->{proto
} ne 'unix') {
5841 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5842 die "migration over non-UNIX sockets not possible\n";
5845 my $socket = $info->{migrate
}->{addr
};
5846 chown $state->{socket_uid
}, -1, $socket;
5847 $state->{sockets
}->{$socket} = 1;
5849 my $unix_sockets = $info->{migrate
}->{unix_sockets
};
5850 foreach my $socket (@$unix_sockets) {
5851 chown $state->{socket_uid
}, -1, $socket;
5852 $state->{sockets
}->{$socket} = 1;
5857 if (PVE
::QemuServer
::qga_check_running
($state->{vmid
})) {
5858 eval { mon_cmd
($state->{vmid
}, "guest-fstrim") };
5859 warn "fstrim failed: $@\n" if $@;
5864 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5868 PVE
::QemuServer
::nbd_stop
($state->{vmid
});
5872 if (PVE
::QemuServer
::Helpers
::vm_running_locally
($state->{vmid
})) {
5873 PVE
::QemuServer
::vm_resume
($state->{vmid
}, 1, 1);
5875 die "VM $state->{vmid} not running\n";
5880 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, $state->{lock});
5881 delete $state->{lock};
5887 my $path = $params->{path
};
5889 die "Not allowed to generate ticket for unknown socket '$path'\n"
5890 if !defined($state->{sockets
}->{$path});
5892 return { ticket
=> PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$path") };
5897 if ($params->{cleanup
}) {
5898 if ($state->{cleanup
}->{fw
}) {
5899 PVE
::Firewall
::remove_vmfw_conf
($state->{vmid
});
5902 for my $volid (keys $state->{cleanup
}->{volumes
}->%*) {
5903 print "freeing volume '$volid' as part of cleanup\n";
5904 eval { PVE
::Storage
::vdisk_free
($state->{storecfg
}, $volid) };
5908 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5911 print "switching to exit-mode, waiting for client to disconnect\n";
5918 my $socket_addr = "/run/qemu-server/$state->{vmid}.mtunnel";
5919 unlink $socket_addr;
5921 $state->{socket} = IO
::Socket
::UNIX-
>new(
5922 Type
=> SOCK_STREAM
(),
5923 Local
=> $socket_addr,
5927 $state->{socket_uid
} = getpwnam('www-data')
5928 or die "Failed to resolve user 'www-data' to numeric UID\n";
5929 chown $state->{socket_uid
}, -1, $socket_addr;
5932 print "mtunnel started\n";
5934 my $conn = eval { PVE
::Tools
::run_with_timeout
(300, sub { $state->{socket}->accept() }) };
5936 warn "Failed to accept tunnel connection - $@\n";
5938 warn "Removing tunnel socket..\n";
5939 unlink $state->{socket};
5941 warn "Removing temporary VM config..\n";
5943 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5946 die "Exiting mtunnel\n";
5949 $state->{conn
} = $conn;
5951 my $reply_err = sub {
5954 my $reply = JSON
::encode_json
({
5955 success
=> JSON
::false
,
5958 $conn->print("$reply\n");
5962 my $reply_ok = sub {
5965 $res->{success
} = JSON
::true
;
5966 my $reply = JSON
::encode_json
($res);
5967 $conn->print("$reply\n");
5971 while (my $line = <$conn>) {
5974 # untaint, we validate below if needed
5975 ($line) = $line =~ /^(.*)$/;
5976 my $parsed = eval { JSON
::decode_json
($line) };
5978 $reply_err->("failed to parse command - $@");
5982 my $cmd = delete $parsed->{cmd
};
5983 if (!defined($cmd)) {
5984 $reply_err->("'cmd' missing");
5985 } elsif ($state->{exit}) {
5986 $reply_err->("tunnel is in exit-mode, processing '$cmd' cmd not possible");
5988 } elsif (my $handler = $cmd_handlers->{$cmd}) {
5989 print "received command '$cmd'\n";
5991 if ($cmd_desc->{$cmd}) {
5992 PVE
::JSONSchema
::validate
($parsed, $cmd_desc->{$cmd});
5996 my $res = $run_locked->($handler, $parsed);
5999 $reply_err->("failed to handle '$cmd' command - $@")
6002 $reply_err->("unknown command '$cmd' given");
6006 if ($state->{exit}) {
6007 print "mtunnel exited\n";
6009 die "mtunnel exited unexpectedly\n";
6013 my $socket_addr = "/run/qemu-server/$vmid.mtunnel";
6014 my $ticket = PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$socket_addr");
6015 my $upid = $rpcenv->fork_worker('qmtunnel', $vmid, $authuser, $realcmd);
6020 socket => $socket_addr,
6024 __PACKAGE__-
>register_method({
6025 name
=> 'mtunnelwebsocket',
6026 path
=> '{vmid}/mtunnelwebsocket',
6029 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.",
6030 user
=> 'all', # check inside
6032 description
=> 'Migration tunnel endpoint for websocket upgrade - only for internal use by VM migration.',
6034 additionalProperties
=> 0,
6036 node
=> get_standard_option
('pve-node'),
6037 vmid
=> get_standard_option
('pve-vmid'),
6040 description
=> "unix socket to forward to",
6044 description
=> "ticket return by initial 'mtunnel' API call, or retrieved via 'ticket' tunnel command",
6051 port
=> { type
=> 'string', optional
=> 1 },
6052 socket => { type
=> 'string', optional
=> 1 },
6058 my $rpcenv = PVE
::RPCEnvironment
::get
();
6059 my $authuser = $rpcenv->get_user();
6061 my $nodename = PVE
::INotify
::nodename
();
6062 my $node = extract_param
($param, 'node');
6064 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
6065 if $node ne 'localhost' && $node ne $nodename;
6067 my $vmid = $param->{vmid
};
6069 PVE
::QemuConfig-
>load_config($vmid);
6071 my $socket = $param->{socket};
6072 PVE
::AccessControl
::verify_tunnel_ticket
($param->{ticket
}, $authuser, "/socket/$socket");
6074 return { socket => $socket };