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.",
3045 'overrule-shutdown' => {
3046 description
=> "Abort any active and visible 'qmshutdown' tasks before stopping",
3059 my $rpcenv = PVE
::RPCEnvironment
::get
();
3060 my $authuser = $rpcenv->get_user();
3062 my $node = extract_param
($param, 'node');
3063 my $vmid = extract_param
($param, 'vmid');
3065 my $skiplock = extract_param
($param, 'skiplock');
3066 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3067 if $skiplock && $authuser ne 'root@pam';
3069 my $keepActive = extract_param
($param, 'keepActive');
3070 raise_param_exc
({ keepActive
=> "Only root may use this option." })
3071 if $keepActive && $authuser ne 'root@pam';
3073 my $migratedfrom = extract_param
($param, 'migratedfrom');
3074 raise_param_exc
({ migratedfrom
=> "Only root may use this option." })
3075 if $migratedfrom && $authuser ne 'root@pam';
3077 my $overrule_shutdown = extract_param
($param, 'overrule-shutdown');
3079 my $storecfg = PVE
::Storage
::config
();
3081 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && ($rpcenv->{type
} ne 'ha') && !defined($migratedfrom)) {
3082 raise_param_exc
({ 'overrule-shutdown' => "Not applicable for HA resources." })
3083 if $overrule_shutdown;
3088 print "Requesting HA stop for VM $vmid\n";
3090 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", '0'];
3091 PVE
::Tools
::run_command
($cmd);
3095 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
3101 syslog
('info', "stop VM $vmid: $upid\n");
3103 if ($overrule_shutdown) {
3104 my $overruled_tasks = PVE
::GuestHelpers
::abort_guest_tasks
(
3105 $rpcenv, 'qmshutdown', $vmid);
3106 my $overruled_tasks_list = join(", ", $overruled_tasks->@*);
3107 print "overruled qmshutdown tasks: $overruled_tasks_list\n"
3108 if @$overruled_tasks;
3111 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0,
3112 $param->{timeout
}, 0, 1, $keepActive, $migratedfrom);
3116 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
3120 __PACKAGE__-
>register_method({
3122 path
=> '{vmid}/status/reset',
3126 description
=> "Reset virtual machine.",
3128 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3131 additionalProperties
=> 0,
3133 node
=> get_standard_option
('pve-node'),
3134 vmid
=> get_standard_option
('pve-vmid',
3135 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3136 skiplock
=> get_standard_option
('skiplock'),
3145 my $rpcenv = PVE
::RPCEnvironment
::get
();
3147 my $authuser = $rpcenv->get_user();
3149 my $node = extract_param
($param, 'node');
3151 my $vmid = extract_param
($param, 'vmid');
3153 my $skiplock = extract_param
($param, 'skiplock');
3154 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3155 if $skiplock && $authuser ne 'root@pam';
3157 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3162 PVE
::QemuServer
::vm_reset
($vmid, $skiplock);
3167 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
3170 __PACKAGE__-
>register_method({
3171 name
=> 'vm_shutdown',
3172 path
=> '{vmid}/status/shutdown',
3176 description
=> "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
3177 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
3179 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3182 additionalProperties
=> 0,
3184 node
=> get_standard_option
('pve-node'),
3185 vmid
=> get_standard_option
('pve-vmid',
3186 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3187 skiplock
=> get_standard_option
('skiplock'),
3189 description
=> "Wait maximal timeout seconds.",
3195 description
=> "Make sure the VM stops.",
3201 description
=> "Do not deactivate storage volumes.",
3214 my $rpcenv = PVE
::RPCEnvironment
::get
();
3215 my $authuser = $rpcenv->get_user();
3217 my $node = extract_param
($param, 'node');
3218 my $vmid = extract_param
($param, 'vmid');
3220 my $skiplock = extract_param
($param, 'skiplock');
3221 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3222 if $skiplock && $authuser ne 'root@pam';
3224 my $keepActive = extract_param
($param, 'keepActive');
3225 raise_param_exc
({ keepActive
=> "Only root may use this option." })
3226 if $keepActive && $authuser ne 'root@pam';
3228 my $storecfg = PVE
::Storage
::config
();
3232 # sending a graceful shutdown command to paused VMs runs into timeouts, and even worse, when
3233 # the VM gets resumed later, it still gets the request delivered and powers off
3234 if (PVE
::QemuServer
::vm_is_paused
($vmid, 1)) {
3235 if ($param->{forceStop
}) {
3236 warn "VM is paused - stop instead of shutdown\n";
3239 die "VM is paused - cannot shutdown\n";
3243 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
3245 my $timeout = $param->{timeout
} // 60;
3249 print "Requesting HA stop for VM $vmid\n";
3251 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", "$timeout"];
3252 PVE
::Tools
::run_command
($cmd);
3256 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
3263 syslog
('info', "shutdown VM $vmid: $upid\n");
3265 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0, $param->{timeout
},
3266 $shutdown, $param->{forceStop
}, $keepActive);
3270 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
3274 __PACKAGE__-
>register_method({
3275 name
=> 'vm_reboot',
3276 path
=> '{vmid}/status/reboot',
3280 description
=> "Reboot the VM by shutting it down, and starting it again. Applies pending changes.",
3282 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3285 additionalProperties
=> 0,
3287 node
=> get_standard_option
('pve-node'),
3288 vmid
=> get_standard_option
('pve-vmid',
3289 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3291 description
=> "Wait maximal timeout seconds for the shutdown.",
3304 my $rpcenv = PVE
::RPCEnvironment
::get
();
3305 my $authuser = $rpcenv->get_user();
3307 my $node = extract_param
($param, 'node');
3308 my $vmid = extract_param
($param, 'vmid');
3310 die "VM is paused - cannot shutdown\n" if PVE
::QemuServer
::vm_is_paused
($vmid, 1);
3312 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3317 syslog
('info', "requesting reboot of VM $vmid: $upid\n");
3318 PVE
::QemuServer
::vm_reboot
($vmid, $param->{timeout
});
3322 return $rpcenv->fork_worker('qmreboot', $vmid, $authuser, $realcmd);
3325 __PACKAGE__-
>register_method({
3326 name
=> 'vm_suspend',
3327 path
=> '{vmid}/status/suspend',
3331 description
=> "Suspend virtual machine.",
3333 description
=> "You need 'VM.PowerMgmt' on /vms/{vmid}, and if you have set 'todisk',".
3334 " you need also 'VM.Config.Disk' on /vms/{vmid} and 'Datastore.AllocateSpace'".
3335 " on the storage for the vmstate.",
3336 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3339 additionalProperties
=> 0,
3341 node
=> get_standard_option
('pve-node'),
3342 vmid
=> get_standard_option
('pve-vmid',
3343 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3344 skiplock
=> get_standard_option
('skiplock'),
3349 description
=> 'If set, suspends the VM to disk. Will be resumed on next VM start.',
3351 statestorage
=> get_standard_option
('pve-storage-id', {
3352 description
=> "The storage for the VM state",
3353 requires
=> 'todisk',
3355 completion
=> \
&PVE
::Storage
::complete_storage_enabled
,
3365 my $rpcenv = PVE
::RPCEnvironment
::get
();
3366 my $authuser = $rpcenv->get_user();
3368 my $node = extract_param
($param, 'node');
3369 my $vmid = extract_param
($param, 'vmid');
3371 my $todisk = extract_param
($param, 'todisk') // 0;
3373 my $statestorage = extract_param
($param, 'statestorage');
3375 my $skiplock = extract_param
($param, 'skiplock');
3376 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3377 if $skiplock && $authuser ne 'root@pam';
3379 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3381 die "Cannot suspend HA managed VM to disk\n"
3382 if $todisk && PVE
::HA
::Config
::vm_is_ha_managed
($vmid);
3384 # early check for storage permission, for better user feedback
3386 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
3387 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3389 # cannot save the state of a non-virtualized PCIe device, so resume cannot really work
3390 for my $key (keys %$conf) {
3391 next if $key !~ /^hostpci\d+/;
3392 die "cannot suspend VM to disk due to passed-through PCI device(s), which lack the"
3393 ." possibility to save/restore their internal state\n";
3396 if (!$statestorage) {
3397 # get statestorage from config if none is given
3398 my $storecfg = PVE
::Storage
::config
();
3399 $statestorage = PVE
::QemuServer
::find_vmstate_storage
($conf, $storecfg);
3402 $rpcenv->check($authuser, "/storage/$statestorage", ['Datastore.AllocateSpace']);
3408 syslog
('info', "suspend VM $vmid: $upid\n");
3410 PVE
::QemuServer
::vm_suspend
($vmid, $skiplock, $todisk, $statestorage);
3415 my $taskname = $todisk ?
'qmsuspend' : 'qmpause';
3416 return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd);
3419 __PACKAGE__-
>register_method({
3420 name
=> 'vm_resume',
3421 path
=> '{vmid}/status/resume',
3425 description
=> "Resume virtual machine.",
3427 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3430 additionalProperties
=> 0,
3432 node
=> get_standard_option
('pve-node'),
3433 vmid
=> get_standard_option
('pve-vmid',
3434 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3435 skiplock
=> get_standard_option
('skiplock'),
3436 nocheck
=> { type
=> 'boolean', optional
=> 1 },
3446 my $rpcenv = PVE
::RPCEnvironment
::get
();
3448 my $authuser = $rpcenv->get_user();
3450 my $node = extract_param
($param, 'node');
3452 my $vmid = extract_param
($param, 'vmid');
3454 my $skiplock = extract_param
($param, 'skiplock');
3455 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3456 if $skiplock && $authuser ne 'root@pam';
3458 # nocheck is used as part of migration when config file might be still
3460 my $nocheck = extract_param
($param, 'nocheck');
3461 raise_param_exc
({ nocheck
=> "Only root may use this option." })
3462 if $nocheck && $authuser ne 'root@pam';
3464 my $to_disk_suspended;
3466 PVE
::QemuConfig-
>lock_config($vmid, sub {
3467 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3468 $to_disk_suspended = PVE
::QemuConfig-
>has_lock($conf, 'suspended');
3472 die "VM $vmid not running\n"
3473 if !$to_disk_suspended && !PVE
::QemuServer
::check_running
($vmid, $nocheck);
3478 syslog
('info', "resume VM $vmid: $upid\n");
3480 if (!$to_disk_suspended) {
3481 PVE
::QemuServer
::vm_resume
($vmid, $skiplock, $nocheck);
3483 my $storecfg = PVE
::Storage
::config
();
3484 PVE
::QemuServer
::vm_start
($storecfg, $vmid, { skiplock
=> $skiplock });
3490 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
3493 __PACKAGE__-
>register_method({
3494 name
=> 'vm_sendkey',
3495 path
=> '{vmid}/sendkey',
3499 description
=> "Send key event to virtual machine.",
3501 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
3504 additionalProperties
=> 0,
3506 node
=> get_standard_option
('pve-node'),
3507 vmid
=> get_standard_option
('pve-vmid',
3508 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3509 skiplock
=> get_standard_option
('skiplock'),
3511 description
=> "The key (qemu monitor encoding).",
3516 returns
=> { type
=> 'null'},
3520 my $rpcenv = PVE
::RPCEnvironment
::get
();
3522 my $authuser = $rpcenv->get_user();
3524 my $node = extract_param
($param, 'node');
3526 my $vmid = extract_param
($param, 'vmid');
3528 my $skiplock = extract_param
($param, 'skiplock');
3529 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3530 if $skiplock && $authuser ne 'root@pam';
3532 PVE
::QemuServer
::vm_sendkey
($vmid, $skiplock, $param->{key
});
3537 __PACKAGE__-
>register_method({
3538 name
=> 'vm_feature',
3539 path
=> '{vmid}/feature',
3543 description
=> "Check if feature for virtual machine is available.",
3545 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3548 additionalProperties
=> 0,
3550 node
=> get_standard_option
('pve-node'),
3551 vmid
=> get_standard_option
('pve-vmid'),
3553 description
=> "Feature to check.",
3555 enum
=> [ 'snapshot', 'clone', 'copy' ],
3557 snapname
=> get_standard_option
('pve-snapshot-name', {
3565 hasFeature
=> { type
=> 'boolean' },
3568 items
=> { type
=> 'string' },
3575 my $node = extract_param
($param, 'node');
3577 my $vmid = extract_param
($param, 'vmid');
3579 my $snapname = extract_param
($param, 'snapname');
3581 my $feature = extract_param
($param, 'feature');
3583 my $running = PVE
::QemuServer
::check_running
($vmid);
3585 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3588 my $snap = $conf->{snapshots
}->{$snapname};
3589 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3592 my $storecfg = PVE
::Storage
::config
();
3594 my $nodelist = PVE
::QemuServer
::shared_nodes
($conf, $storecfg);
3595 my $hasFeature = PVE
::QemuConfig-
>has_feature($feature, $conf, $storecfg, $snapname, $running);
3598 hasFeature
=> $hasFeature,
3599 nodes
=> [ keys %$nodelist ],
3603 __PACKAGE__-
>register_method({
3605 path
=> '{vmid}/clone',
3609 description
=> "Create a copy of virtual machine/template.",
3611 description
=> "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
3612 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
3613 "'Datastore.AllocateSpace' on any used storage and 'SDN.Use' on any used bridge/vnet",
3616 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
3618 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
3619 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param
=> 'pool'],
3624 additionalProperties
=> 0,
3626 node
=> get_standard_option
('pve-node'),
3627 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3628 newid
=> get_standard_option
('pve-vmid', {
3629 completion
=> \
&PVE
::Cluster
::complete_next_vmid
,
3630 description
=> 'VMID for the clone.' }),
3633 type
=> 'string', format
=> 'dns-name',
3634 description
=> "Set a name for the new VM.",
3639 description
=> "Description for the new VM.",
3643 type
=> 'string', format
=> 'pve-poolid',
3644 description
=> "Add the new VM to the specified pool.",
3646 snapname
=> get_standard_option
('pve-snapshot-name', {
3649 storage
=> get_standard_option
('pve-storage-id', {
3650 description
=> "Target storage for full clone.",
3654 description
=> "Target format for file storage. Only valid for full clone.",
3657 enum
=> [ 'raw', 'qcow2', 'vmdk'],
3662 description
=> "Create a full copy of all disks. This is always done when " .
3663 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
3665 target
=> get_standard_option
('pve-node', {
3666 description
=> "Target node. Only allowed if the original VM is on shared storage.",
3670 description
=> "Override I/O bandwidth limit (in KiB/s).",
3674 default => 'clone limit from datacenter or storage config',
3684 my $rpcenv = PVE
::RPCEnvironment
::get
();
3685 my $authuser = $rpcenv->get_user();
3687 my $node = extract_param
($param, 'node');
3688 my $vmid = extract_param
($param, 'vmid');
3689 my $newid = extract_param
($param, 'newid');
3690 my $pool = extract_param
($param, 'pool');
3692 my $snapname = extract_param
($param, 'snapname');
3693 my $storage = extract_param
($param, 'storage');
3694 my $format = extract_param
($param, 'format');
3695 my $target = extract_param
($param, 'target');
3697 my $localnode = PVE
::INotify
::nodename
();
3699 if ($target && ($target eq $localnode || $target eq 'localhost')) {
3703 my $running = PVE
::QemuServer
::check_running
($vmid) || 0;
3705 my $load_and_check = sub {
3706 $rpcenv->check_pool_exist($pool) if defined($pool);
3707 PVE
::Cluster
::check_node_exists
($target) if $target;
3709 my $storecfg = PVE
::Storage
::config
();
3712 # check if storage is enabled on local node
3713 PVE
::Storage
::storage_check_enabled
($storecfg, $storage);
3715 # check if storage is available on target node
3716 PVE
::Storage
::storage_check_enabled
($storecfg, $storage, $target);
3717 # clone only works if target storage is shared
3718 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storage);
3719 die "can't clone to non-shared storage '$storage'\n"
3720 if !$scfg->{shared
};
3724 PVE
::Cluster
::check_cfs_quorum
();
3726 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3727 PVE
::QemuConfig-
>check_lock($conf);
3729 my $verify_running = PVE
::QemuServer
::check_running
($vmid) || 0;
3730 die "unexpected state change\n" if $verify_running != $running;
3732 die "snapshot '$snapname' does not exist\n"
3733 if $snapname && !defined( $conf->{snapshots
}->{$snapname});
3735 my $full = $param->{full
} // !PVE
::QemuConfig-
>is_template($conf);
3737 die "parameter 'storage' not allowed for linked clones\n"
3738 if defined($storage) && !$full;
3740 die "parameter 'format' not allowed for linked clones\n"
3741 if defined($format) && !$full;
3743 my $oldconf = $snapname ?
$conf->{snapshots
}->{$snapname} : $conf;
3745 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
3746 PVE
::QemuServer
::check_mapping_access
($rpcenv, $authuser, $oldconf);
3748 PVE
::QemuServer
::check_bridge_access
($rpcenv, $authuser, $oldconf);
3750 die "can't clone VM to node '$target' (VM uses local storage)\n"
3751 if $target && !$sharedvm;
3753 my $conffile = PVE
::QemuConfig-
>config_file($newid);
3754 die "unable to create VM $newid: config file already exists\n"
3757 my $newconf = { lock => 'clone' };
3762 foreach my $opt (keys %$oldconf) {
3763 my $value = $oldconf->{$opt};
3765 # do not copy snapshot related info
3766 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
3767 $opt eq 'vmstate' || $opt eq 'snapstate';
3769 # no need to copy unused images, because VMID(owner) changes anyways
3770 next if $opt =~ m/^unused\d+$/;
3772 die "cannot clone TPM state while VM is running\n"
3773 if $full && $running && !$snapname && $opt eq 'tpmstate0';
3775 # always change MAC! address
3776 if ($opt =~ m/^net(\d+)$/) {
3777 my $net = PVE
::QemuServer
::parse_net
($value);
3778 my $dc = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
3779 $net->{macaddr
} = PVE
::Tools
::random_ether_addr
($dc->{mac_prefix
});
3780 $newconf->{$opt} = PVE
::QemuServer
::print_net
($net);
3781 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
3782 my $drive = PVE
::QemuServer
::parse_drive
($opt, $value);
3783 die "unable to parse drive options for '$opt'\n" if !$drive;
3784 if (PVE
::QemuServer
::drive_is_cdrom
($drive, 1)) {
3785 $newconf->{$opt} = $value; # simply copy configuration
3787 if ($full || PVE
::QemuServer
::drive_is_cloudinit
($drive)) {
3788 die "Full clone feature is not supported for drive '$opt'\n"
3789 if !PVE
::Storage
::volume_has_feature
($storecfg, 'copy', $drive->{file
}, $snapname, $running);
3790 $fullclone->{$opt} = 1;
3792 # not full means clone instead of copy
3793 die "Linked clone feature is not supported for drive '$opt'\n"
3794 if !PVE
::Storage
::volume_has_feature
($storecfg, 'clone', $drive->{file
}, $snapname, $running);
3796 $drives->{$opt} = $drive;
3797 next if PVE
::QemuServer
::drive_is_cloudinit
($drive);
3798 push @$vollist, $drive->{file
};
3801 # copy everything else
3802 $newconf->{$opt} = $value;
3806 return ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone);
3810 my ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone) = $load_and_check->();
3811 my $storecfg = PVE
::Storage
::config
();
3813 # auto generate a new uuid
3814 my $smbios1 = PVE
::QemuServer
::parse_smbios1
($newconf->{smbios1
} || '');
3815 $smbios1->{uuid
} = PVE
::QemuServer
::generate_uuid
();
3816 $newconf->{smbios1
} = PVE
::QemuServer
::print_smbios1
($smbios1);
3817 # auto generate a new vmgenid only if the option was set for template
3818 if ($newconf->{vmgenid
}) {
3819 $newconf->{vmgenid
} = PVE
::QemuServer
::generate_uuid
();
3822 delete $newconf->{template
};
3824 if ($param->{name
}) {
3825 $newconf->{name
} = $param->{name
};
3827 $newconf->{name
} = "Copy-of-VM-" . ($oldconf->{name
} // $vmid);
3830 if ($param->{description
}) {
3831 $newconf->{description
} = $param->{description
};
3834 # create empty/temp config - this fails if VM already exists on other node
3835 # FIXME use PVE::QemuConfig->create_and_lock_config and adapt code
3836 PVE
::Tools
::file_set_contents
($conffile, "# qmclone temporary file\nlock: clone\n");
3838 PVE
::Firewall
::clone_vmfw_conf
($vmid, $newid);
3840 my $newvollist = [];
3847 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
3849 PVE
::Storage
::activate_volumes
($storecfg, $vollist, $snapname);
3851 my $bwlimit = extract_param
($param, 'bwlimit');
3853 my $total_jobs = scalar(keys %{$drives});
3856 foreach my $opt (sort keys %$drives) {
3857 my $drive = $drives->{$opt};
3858 my $skipcomplete = ($total_jobs != $i); # finish after last drive
3859 my $completion = $skipcomplete ?
'skip' : 'complete';
3861 my $src_sid = PVE
::Storage
::parse_volume_id
($drive->{file
});
3862 my $storage_list = [ $src_sid ];
3863 push @$storage_list, $storage if defined($storage);
3864 my $clonelimit = PVE
::Storage
::get_bandwidth_limit
('clone', $storage_list, $bwlimit);
3868 running
=> $running,
3871 snapname
=> $snapname,
3877 storage
=> $storage,
3881 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($oldconf)
3882 if $opt eq 'efidisk0';
3884 my $newdrive = PVE
::QemuServer
::clone_disk
(
3896 $newconf->{$opt} = PVE
::QemuServer
::print_drive
($newdrive);
3898 PVE
::QemuConfig-
>write_config($newid, $newconf);
3902 delete $newconf->{lock};
3904 # do not write pending changes
3905 if (my @changes = keys %{$newconf->{pending
}}) {
3906 my $pending = join(',', @changes);
3907 warn "found pending changes for '$pending', discarding for clone\n";
3908 delete $newconf->{pending
};
3911 PVE
::QemuConfig-
>write_config($newid, $newconf);
3913 PVE
::QemuServer
::create_ifaces_ipams_ips
($newconf, $newid);
3917 # always deactivate volumes – avoids that LVM LVs are active on several nodes
3918 eval { PVE
::Storage
::deactivate_volumes
($storecfg, $vollist, $snapname) };
3919 # but only warn when that fails (e.g., parallel clones keeping them active)
3923 PVE
::Storage
::deactivate_volumes
($storecfg, $newvollist);
3925 my $newconffile = PVE
::QemuConfig-
>config_file($newid, $target);
3926 die "Failed to move config to node '$target' - rename failed: $!\n"
3927 if !rename($conffile, $newconffile);
3930 PVE
::AccessControl
::add_vm_to_pool
($newid, $pool) if $pool;
3933 eval { PVE
::QemuServer
::qemu_blockjobs_cancel
($vmid, $jobs) };
3934 sleep 1; # some storage like rbd need to wait before release volume - really?
3936 foreach my $volid (@$newvollist) {
3937 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
3941 PVE
::Firewall
::remove_vmfw_conf
($newid);
3943 unlink $conffile; # avoid races -> last thing before die
3945 die "clone failed: $err";
3951 # Aquire exclusive lock lock for $newid
3952 my $lock_target_vm = sub {
3953 return PVE
::QemuConfig-
>lock_config_full($newid, 1, $clonefn);
3956 my $lock_source_vm = sub {
3957 # exclusive lock if VM is running - else shared lock is enough;
3959 return PVE
::QemuConfig-
>lock_config_full($vmid, 1, $lock_target_vm);
3961 return PVE
::QemuConfig-
>lock_config_shared($vmid, 1, $lock_target_vm);
3965 $load_and_check->(); # early checks before forking/locking
3967 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $lock_source_vm);
3970 __PACKAGE__-
>register_method({
3971 name
=> 'move_vm_disk',
3972 path
=> '{vmid}/move_disk',
3976 description
=> "Move volume to different storage or to a different VM.",
3978 description
=> "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
3979 "and 'Datastore.AllocateSpace' permissions on the storage. To move ".
3980 "a disk to another VM, you need the permissions on the target VM as well.",
3981 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
3984 additionalProperties
=> 0,
3986 node
=> get_standard_option
('pve-node'),
3987 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3988 'target-vmid' => get_standard_option
('pve-vmid', {
3989 completion
=> \
&PVE
::QemuServer
::complete_vmid
,
3994 description
=> "The disk you want to move.",
3995 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
3997 storage
=> get_standard_option
('pve-storage-id', {
3998 description
=> "Target storage.",
3999 completion
=> \
&PVE
::QemuServer
::complete_storage
,
4004 description
=> "Target Format.",
4005 enum
=> [ 'raw', 'qcow2', 'vmdk' ],
4010 description
=> "Delete the original disk after successful copy. By default the"
4011 ." original disk is kept as unused disk.",
4017 description
=> 'Prevent changes if current configuration file has different SHA1"
4018 ." digest. This can be used to prevent concurrent modifications.',
4023 description
=> "Override I/O bandwidth limit (in KiB/s).",
4027 default => 'move limit from datacenter or storage config',
4031 description
=> "The config key the disk will be moved to on the target VM"
4032 ." (for example, ide0 or scsi1). Default is the source disk key.",
4033 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
4036 'target-digest' => {
4038 description
=> 'Prevent changes if the current config file of the target VM has a"
4039 ." different SHA1 digest. This can be used to detect concurrent modifications.',
4047 description
=> "the task ID.",
4052 my $rpcenv = PVE
::RPCEnvironment
::get
();
4053 my $authuser = $rpcenv->get_user();
4055 my $node = extract_param
($param, 'node');
4056 my $vmid = extract_param
($param, 'vmid');
4057 my $target_vmid = extract_param
($param, 'target-vmid');
4058 my $digest = extract_param
($param, 'digest');
4059 my $target_digest = extract_param
($param, 'target-digest');
4060 my $disk = extract_param
($param, 'disk');
4061 my $target_disk = extract_param
($param, 'target-disk') // $disk;
4062 my $storeid = extract_param
($param, 'storage');
4063 my $format = extract_param
($param, 'format');
4065 my $storecfg = PVE
::Storage
::config
();
4067 my $load_and_check_move = sub {
4068 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4069 PVE
::QemuConfig-
>check_lock($conf);
4071 PVE
::Tools
::assert_if_modified
($digest, $conf->{digest
});
4073 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4075 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
4077 die "disk '$disk' has no associated volume\n" if !$drive->{file
};
4078 die "you can't move a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
4080 my $old_volid = $drive->{file
};
4082 my ($oldstoreid, $oldvolname) = PVE
::Storage
::parse_volume_id
($old_volid);
4083 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
4087 die "you can't move to the same storage with same format\n"
4088 if $oldstoreid eq $storeid && (!$format || !$oldfmt || $oldfmt eq $format);
4090 # this only checks snapshots because $disk is passed!
4091 my $snapshotted = PVE
::QemuServer
::Drive
::is_volume_in_use
(
4097 die "you can't move a disk with snapshots and delete the source\n"
4098 if $snapshotted && $param->{delete};
4100 return ($conf, $drive, $oldstoreid, $snapshotted);
4103 my $move_updatefn = sub {
4104 my ($conf, $drive, $oldstoreid, $snapshotted) = $load_and_check_move->();
4105 my $old_volid = $drive->{file
};
4107 PVE
::Cluster
::log_msg
(
4110 "move disk VM $vmid: move --disk $disk --storage $storeid"
4113 my $running = PVE
::QemuServer
::check_running
($vmid);
4115 PVE
::Storage
::activate_volumes
($storecfg, [ $drive->{file
} ]);
4117 my $newvollist = [];
4123 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
4125 warn "moving disk with snapshots, snapshots will not be moved!\n"
4128 my $bwlimit = extract_param
($param, 'bwlimit');
4129 my $movelimit = PVE
::Storage
::get_bandwidth_limit
(
4131 [$oldstoreid, $storeid],
4137 running
=> $running,
4146 storage
=> $storeid,
4150 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($conf)
4151 if $disk eq 'efidisk0';
4153 my $newdrive = PVE
::QemuServer
::clone_disk
(
4164 $conf->{$disk} = PVE
::QemuServer
::print_drive
($newdrive);
4166 PVE
::QemuConfig-
>add_unused_volume($conf, $old_volid) if !$param->{delete};
4168 # convert moved disk to base if part of template
4169 PVE
::QemuServer
::template_create
($vmid, $conf, $disk)
4170 if PVE
::QemuConfig-
>is_template($conf);
4172 PVE
::QemuConfig-
>write_config($vmid, $conf);
4174 my $do_trim = PVE
::QemuServer
::get_qga_key
($conf, 'fstrim_cloned_disks');
4175 if ($running && $do_trim && PVE
::QemuServer
::qga_check_running
($vmid)) {
4176 eval { mon_cmd
($vmid, "guest-fstrim") };
4180 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
4181 PVE
::Storage
::deactivate_volumes
($storecfg, [ $newdrive->{file
} ])
4187 foreach my $volid (@$newvollist) {
4188 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid) };
4191 die "storage migration failed: $err";
4194 if ($param->{delete}) {
4196 PVE
::Storage
::deactivate_volumes
($storecfg, [$old_volid]);
4197 PVE
::Storage
::vdisk_free
($storecfg, $old_volid);
4203 my $load_and_check_reassign_configs = sub {
4204 my $vmlist = PVE
::Cluster
::get_vmlist
()->{ids
};
4206 die "could not find VM ${vmid}\n" if !exists($vmlist->{$vmid});
4207 die "could not find target VM ${target_vmid}\n" if !exists($vmlist->{$target_vmid});
4209 my $source_node = $vmlist->{$vmid}->{node
};
4210 my $target_node = $vmlist->{$target_vmid}->{node
};
4212 die "Both VMs need to be on the same node ($source_node != $target_node)\n"
4213 if $source_node ne $target_node;
4215 my $source_conf = PVE
::QemuConfig-
>load_config($vmid);
4216 PVE
::QemuConfig-
>check_lock($source_conf);
4217 my $target_conf = PVE
::QemuConfig-
>load_config($target_vmid);
4218 PVE
::QemuConfig-
>check_lock($target_conf);
4220 die "Can't move disks from or to template VMs\n"
4221 if ($source_conf->{template
} || $target_conf->{template
});
4224 eval { PVE
::Tools
::assert_if_modified
($digest, $source_conf->{digest
}) };
4225 die "VM ${vmid}: $@" if $@;
4228 if ($target_digest) {
4229 eval { PVE
::Tools
::assert_if_modified
($target_digest, $target_conf->{digest
}) };
4230 die "VM ${target_vmid}: $@" if $@;
4233 die "Disk '${disk}' for VM '$vmid' does not exist\n" if !defined($source_conf->{$disk});
4235 die "Target disk key '${target_disk}' is already in use for VM '$target_vmid'\n"
4236 if $target_conf->{$target_disk};
4238 my $drive = PVE
::QemuServer
::parse_drive
(
4240 $source_conf->{$disk},
4242 die "failed to parse source disk - $@\n" if !$drive;
4244 my $source_volid = $drive->{file
};
4246 die "disk '${disk}' has no associated volume\n" if !$source_volid;
4247 die "CD drive contents can't be moved to another VM\n"
4248 if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
4250 my $storeid = PVE
::Storage
::parse_volume_id
($source_volid, 1);
4251 die "Volume '$source_volid' not managed by PVE\n" if !defined($storeid);
4253 die "Can't move disk used by a snapshot to another VM\n"
4254 if PVE
::QemuServer
::Drive
::is_volume_in_use
($storecfg, $source_conf, $disk, $source_volid);
4255 die "Storage does not support moving of this disk to another VM\n"
4256 if (!PVE
::Storage
::volume_has_feature
($storecfg, 'rename', $source_volid));
4257 die "Cannot move disk to another VM while the source VM is running - detach first\n"
4258 if PVE
::QemuServer
::check_running
($vmid) && $disk !~ m/^unused\d+$/;
4260 # now re-parse using target disk slot format
4261 if ($target_disk =~ /^unused\d+$/) {
4262 $drive = PVE
::QemuServer
::parse_drive
(
4267 $drive = PVE
::QemuServer
::parse_drive
(
4269 $source_conf->{$disk},
4272 die "failed to parse source disk for target disk format - $@\n" if !$drive;
4274 my $repl_conf = PVE
::ReplicationConfig-
>new();
4275 if ($repl_conf->check_for_existing_jobs($target_vmid, 1)) {
4276 my $format = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4277 die "Cannot move disk to a replicated VM. Storage does not support replication!\n"
4278 if !PVE
::Storage
::storage_can_replicate
($storecfg, $storeid, $format);
4281 return ($source_conf, $target_conf, $drive);
4286 print STDERR
"$msg\n";
4289 my $disk_reassignfn = sub {
4290 return PVE
::QemuConfig-
>lock_config($vmid, sub {
4291 return PVE
::QemuConfig-
>lock_config($target_vmid, sub {
4292 my ($source_conf, $target_conf, $drive) = &$load_and_check_reassign_configs();
4294 my $source_volid = $drive->{file
};
4296 print "moving disk '$disk' from VM '$vmid' to '$target_vmid'\n";
4297 my ($storeid, $source_volname) = PVE
::Storage
::parse_volume_id
($source_volid);
4299 my $fmt = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4301 my $new_volid = PVE
::Storage
::rename_volume
(
4307 $drive->{file
} = $new_volid;
4309 my $boot_order = PVE
::QemuServer
::device_bootorder
($source_conf);
4310 if (defined(delete $boot_order->{$disk})) {
4311 print "removing disk '$disk' from boot order config\n";
4312 my $boot_devs = [ sort { $boot_order->{$a} <=> $boot_order->{$b} } keys %$boot_order ];
4313 $source_conf->{boot
} = PVE
::QemuServer
::print_bootorder
($boot_devs);
4316 delete $source_conf->{$disk};
4317 print "removing disk '${disk}' from VM '${vmid}' config\n";
4318 PVE
::QemuConfig-
>write_config($vmid, $source_conf);
4320 my $drive_string = PVE
::QemuServer
::print_drive
($drive);
4322 if ($target_disk =~ /^unused\d+$/) {
4323 $target_conf->{$target_disk} = $drive_string;
4324 PVE
::QemuConfig-
>write_config($target_vmid, $target_conf);
4329 vmid
=> $target_vmid,
4330 digest
=> $target_digest,
4331 $target_disk => $drive_string,
4337 # remove possible replication snapshots
4338 if (PVE
::Storage
::volume_has_feature
(
4344 PVE
::Replication
::prepare
(
4354 print "Failed to remove replication snapshots on moved disk " .
4355 "'$target_disk'. Manual cleanup could be necessary.\n";
4362 if ($target_vmid && $storeid) {
4363 my $msg = "either set 'storage' or 'target-vmid', but not both";
4364 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4365 } elsif ($target_vmid) {
4366 $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
4367 if $authuser ne 'root@pam';
4369 raise_param_exc
({ 'target-vmid' => "must be different than source VMID to reassign disk" })
4370 if $vmid eq $target_vmid;
4372 my (undef, undef, $drive) = &$load_and_check_reassign_configs();
4373 my $storage = PVE
::Storage
::parse_volume_id
($drive->{file
});
4374 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
4376 return $rpcenv->fork_worker(
4378 "${vmid}-${disk}>${target_vmid}-${target_disk}",
4382 } elsif ($storeid) {
4383 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4385 $load_and_check_move->(); # early checks before forking/locking
4388 PVE
::QemuConfig-
>lock_config($vmid, $move_updatefn);
4391 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
4393 my $msg = "both 'storage' and 'target-vmid' missing, either needs to be set";
4394 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4398 my $check_vm_disks_local = sub {
4399 my ($storecfg, $vmconf, $vmid) = @_;
4401 my $local_disks = {};
4403 # add some more information to the disks e.g. cdrom
4404 PVE
::QemuServer
::foreach_volid
($vmconf, sub {
4405 my ($volid, $attr) = @_;
4407 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
4409 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
4410 return if $scfg->{shared
};
4412 # The shared attr here is just a special case where the vdisk
4413 # is marked as shared manually
4414 return if $attr->{shared
};
4415 return if $attr->{cdrom
} and $volid eq "none";
4417 if (exists $local_disks->{$volid}) {
4418 @{$local_disks->{$volid}}{keys %$attr} = values %$attr
4420 $local_disks->{$volid} = $attr;
4421 # ensure volid is present in case it's needed
4422 $local_disks->{$volid}->{volid
} = $volid;
4426 return $local_disks;
4429 __PACKAGE__-
>register_method({
4430 name
=> 'migrate_vm_precondition',
4431 path
=> '{vmid}/migrate',
4435 description
=> "Get preconditions for migration.",
4437 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4440 additionalProperties
=> 0,
4442 node
=> get_standard_option
('pve-node'),
4443 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4444 target
=> get_standard_option
('pve-node', {
4445 description
=> "Target node.",
4446 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4454 running
=> { type
=> 'boolean' },
4458 description
=> "List nodes allowed for offline migration, only passed if VM is offline"
4460 not_allowed_nodes
=> {
4463 description
=> "List not allowed nodes with additional informations, only passed if VM is offline"
4467 description
=> "List local disks including CD-Rom, unsused and not referenced disks"
4469 local_resources
=> {
4471 description
=> "List local resources e.g. pci, usb"
4473 'mapped-resources' => {
4475 description
=> "List of mapped resources e.g. pci, usb"
4482 my $rpcenv = PVE
::RPCEnvironment
::get
();
4484 my $authuser = $rpcenv->get_user();
4486 PVE
::Cluster
::check_cfs_quorum
();
4490 my $vmid = extract_param
($param, 'vmid');
4491 my $target = extract_param
($param, 'target');
4492 my $localnode = PVE
::INotify
::nodename
();
4496 my $vmconf = PVE
::QemuConfig-
>load_config($vmid);
4497 my $storecfg = PVE
::Storage
::config
();
4500 # try to detect errors early
4501 PVE
::QemuConfig-
>check_lock($vmconf);
4503 $res->{running
} = PVE
::QemuServer
::check_running
($vmid) ?
1:0;
4505 my ($local_resources, $mapped_resources, $missing_mappings_by_node) =
4506 PVE
::QemuServer
::check_local_resources
($vmconf, 1);
4507 delete $missing_mappings_by_node->{$localnode};
4509 my $vga = PVE
::QemuServer
::parse_vga
($vmconf->{vga
});
4510 if ($res->{running
} && $vga->{'clipboard'} && $vga->{'clipboard'} eq 'vnc') {
4511 push $local_resources->@*, "clipboard=vnc";
4514 # if vm is not running, return target nodes where local storage/mapped devices are available
4515 # for offline migration
4516 if (!$res->{running
}) {
4517 $res->{allowed_nodes
} = [];
4518 my $checked_nodes = PVE
::QemuServer
::check_local_storage_availability
($vmconf, $storecfg);
4519 delete $checked_nodes->{$localnode};
4521 foreach my $node (keys %$checked_nodes) {
4522 my $missing_mappings = $missing_mappings_by_node->{$node};
4523 if (scalar($missing_mappings->@*)) {
4524 $checked_nodes->{$node}->{'unavailable-resources'} = $missing_mappings;
4528 if (!defined($checked_nodes->{$node}->{unavailable_storages
})) {
4529 push @{$res->{allowed_nodes
}}, $node;
4533 $res->{not_allowed_nodes
} = $checked_nodes;
4536 my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
4537 $res->{local_disks
} = [ values %$local_disks ];;
4539 $res->{local_resources
} = $local_resources;
4540 $res->{'mapped-resources'} = $mapped_resources;
4547 __PACKAGE__-
>register_method({
4548 name
=> 'migrate_vm',
4549 path
=> '{vmid}/migrate',
4553 description
=> "Migrate virtual machine. Creates a new migration task.",
4555 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4558 additionalProperties
=> 0,
4560 node
=> get_standard_option
('pve-node'),
4561 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4562 target
=> get_standard_option
('pve-node', {
4563 description
=> "Target node.",
4564 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4568 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4573 description
=> "Allow to migrate VMs which use local devices. Only root may use this option.",
4578 enum
=> ['secure', 'insecure'],
4579 description
=> "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
4582 migration_network
=> {
4583 type
=> 'string', format
=> 'CIDR',
4584 description
=> "CIDR of the (sub) network that is used for migration.",
4587 "with-local-disks" => {
4589 description
=> "Enable live storage migration for local disk",
4592 targetstorage
=> get_standard_option
('pve-targetstorage', {
4593 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4596 description
=> "Override I/O bandwidth limit (in KiB/s).",
4600 default => 'migrate limit from datacenter or storage config',
4606 description
=> "the task ID.",
4611 my $rpcenv = PVE
::RPCEnvironment
::get
();
4612 my $authuser = $rpcenv->get_user();
4614 my $target = extract_param
($param, 'target');
4616 my $localnode = PVE
::INotify
::nodename
();
4617 raise_param_exc
({ target
=> "target is local node."}) if $target eq $localnode;
4619 PVE
::Cluster
::check_cfs_quorum
();
4621 PVE
::Cluster
::check_node_exists
($target);
4623 my $targetip = PVE
::Cluster
::remote_node_ip
($target);
4625 my $vmid = extract_param
($param, 'vmid');
4627 raise_param_exc
({ force
=> "Only root may use this option." })
4628 if $param->{force
} && $authuser ne 'root@pam';
4630 raise_param_exc
({ migration_type
=> "Only root may use this option." })
4631 if $param->{migration_type
} && $authuser ne 'root@pam';
4633 # allow root only until better network permissions are available
4634 raise_param_exc
({ migration_network
=> "Only root may use this option." })
4635 if $param->{migration_network
} && $authuser ne 'root@pam';
4638 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4640 # try to detect errors early
4642 PVE
::QemuConfig-
>check_lock($conf);
4644 if (PVE
::QemuServer
::check_running
($vmid)) {
4645 die "can't migrate running VM without --online\n" if !$param->{online
};
4647 my $repl_conf = PVE
::ReplicationConfig-
>new();
4648 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
4649 my $is_replicated_to_target = defined($repl_conf->find_local_replication_job($vmid, $target));
4650 if (!$param->{force
} && $is_replicated && !$is_replicated_to_target) {
4651 die "Cannot live-migrate replicated VM to node '$target' - not a replication " .
4652 "target. Use 'force' to override.\n";
4655 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4656 $param->{online
} = 0;
4659 my $storecfg = PVE
::Storage
::config
();
4660 if (my $targetstorage = $param->{targetstorage
}) {
4661 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($targetstorage, 'pve-storage-id') };
4662 raise_param_exc
({ targetstorage
=> "failed to parse storage map: $@" })
4665 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk'])
4666 if !defined($storagemap->{identity
});
4668 foreach my $target_sid (values %{$storagemap->{entries
}}) {
4669 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $target_sid, $target);
4672 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storagemap->{default}, $target)
4673 if $storagemap->{default};
4675 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target)
4676 if $storagemap->{identity
};
4678 $param->{storagemap
} = $storagemap;
4680 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target);
4683 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
4688 print "Requesting HA migration for VM $vmid to node $target\n";
4690 my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
4691 PVE
::Tools
::run_command
($cmd);
4695 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
4700 PVE
::QemuMigrate-
>migrate($target, $targetip, $vmid, $param);
4704 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
4707 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
4712 __PACKAGE__-
>register_method({
4713 name
=> 'remote_migrate_vm',
4714 path
=> '{vmid}/remote_migrate',
4718 description
=> "Migrate virtual machine to a remote cluster. Creates a new migration task. EXPERIMENTAL feature!",
4720 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4723 additionalProperties
=> 0,
4725 node
=> get_standard_option
('pve-node'),
4726 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4727 'target-vmid' => get_standard_option
('pve-vmid', { optional
=> 1 }),
4728 'target-endpoint' => get_standard_option
('proxmox-remote', {
4729 description
=> "Remote target endpoint",
4733 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4738 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.",
4742 'target-storage' => get_standard_option
('pve-targetstorage', {
4743 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4746 'target-bridge' => {
4748 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.",
4749 format
=> 'bridge-pair-list',
4752 description
=> "Override I/O bandwidth limit (in KiB/s).",
4756 default => 'migrate limit from datacenter or storage config',
4762 description
=> "the task ID.",
4767 my $rpcenv = PVE
::RPCEnvironment
::get
();
4768 my $authuser = $rpcenv->get_user();
4770 my $source_vmid = extract_param
($param, 'vmid');
4771 my $target_endpoint = extract_param
($param, 'target-endpoint');
4772 my $target_vmid = extract_param
($param, 'target-vmid') // $source_vmid;
4774 my $delete = extract_param
($param, 'delete') // 0;
4776 PVE
::Cluster
::check_cfs_quorum
();
4779 my $conf = PVE
::QemuConfig-
>load_config($source_vmid);
4781 PVE
::QemuConfig-
>check_lock($conf);
4783 raise_param_exc
({ vmid
=> "cannot migrate HA-managed VM to remote cluster" })
4784 if PVE
::HA
::Config
::vm_is_ha_managed
($source_vmid);
4786 my $remote = PVE
::JSONSchema
::parse_property_string
('proxmox-remote', $target_endpoint);
4788 # TODO: move this as helper somewhere appropriate?
4790 protocol
=> 'https',
4791 host
=> $remote->{host
},
4792 port
=> $remote->{port
} // 8006,
4793 apitoken
=> $remote->{apitoken
},
4797 if ($fp = $remote->{fingerprint
}) {
4798 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 };
4801 print "Establishing API connection with remote at '$remote->{host}'\n";
4803 my $api_client = PVE
::APIClient
::LWP-
>new(%$conn_args);
4805 if (!defined($fp)) {
4806 my $cert_info = $api_client->get("/nodes/localhost/certificates/info");
4807 foreach my $cert (@$cert_info) {
4808 my $filename = $cert->{filename
};
4809 next if $filename ne 'pveproxy-ssl.pem' && $filename ne 'pve-ssl.pem';
4810 $fp = $cert->{fingerprint
} if !$fp || $filename eq 'pveproxy-ssl.pem';
4812 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 }
4816 my $repl_conf = PVE
::ReplicationConfig-
>new();
4817 my $is_replicated = $repl_conf->check_for_existing_jobs($source_vmid, 1);
4818 die "cannot remote-migrate replicated VM\n" if $is_replicated;
4820 if (PVE
::QemuServer
::check_running
($source_vmid)) {
4821 die "can't migrate running VM without --online\n" if !$param->{online
};
4824 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4825 $param->{online
} = 0;
4828 my $storecfg = PVE
::Storage
::config
();
4829 my $target_storage = extract_param
($param, 'target-storage');
4830 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($target_storage, 'pve-storage-id') };
4831 raise_param_exc
({ 'target-storage' => "failed to parse storage map: $@" })
4834 my $target_bridge = extract_param
($param, 'target-bridge');
4835 my $bridgemap = eval { PVE
::JSONSchema
::parse_idmap
($target_bridge, 'pve-bridge-id') };
4836 raise_param_exc
({ 'target-bridge' => "failed to parse bridge map: $@" })
4839 die "remote migration requires explicit storage mapping!\n"
4840 if $storagemap->{identity
};
4842 $param->{storagemap
} = $storagemap;
4843 $param->{bridgemap
} = $bridgemap;
4844 $param->{remote
} = {
4845 conn
=> $conn_args, # re-use fingerprint for tunnel
4846 client
=> $api_client,
4847 vmid
=> $target_vmid,
4849 $param->{migration_type
} = 'websocket';
4850 $param->{'with-local-disks'} = 1;
4851 $param->{delete} = $delete if $delete;
4853 my $cluster_status = $api_client->get("/cluster/status");
4855 foreach my $entry (@$cluster_status) {
4856 next if $entry->{type
} ne 'node';
4857 if ($entry->{local}) {
4858 $target_node = $entry->{name
};
4863 die "couldn't determine endpoint's node name\n"
4864 if !defined($target_node);
4867 PVE
::QemuMigrate-
>migrate($target_node, $remote->{host
}, $source_vmid, $param);
4871 return PVE
::GuestHelpers
::guest_migration_lock
($source_vmid, 10, $realcmd);
4874 return $rpcenv->fork_worker('qmigrate', $source_vmid, $authuser, $worker);
4877 __PACKAGE__-
>register_method({
4879 path
=> '{vmid}/monitor',
4883 description
=> "Execute QEMU monitor commands.",
4885 description
=> "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
4886 check
=> ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
4889 additionalProperties
=> 0,
4891 node
=> get_standard_option
('pve-node'),
4892 vmid
=> get_standard_option
('pve-vmid'),
4895 description
=> "The monitor command.",
4899 returns
=> { type
=> 'string'},
4903 my $rpcenv = PVE
::RPCEnvironment
::get
();
4904 my $authuser = $rpcenv->get_user();
4907 my $command = shift;
4908 return $command =~ m/^\s*info(\s+|$)/
4909 || $command =~ m/^\s*help\s*$/;
4912 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
4913 if !&$is_ro($param->{command
});
4915 my $vmid = $param->{vmid
};
4917 my $conf = PVE
::QemuConfig-
>load_config ($vmid); # check if VM exists
4921 $res = PVE
::QemuServer
::Monitor
::hmp_cmd
($vmid, $param->{command
});
4923 $res = "ERROR: $@" if $@;
4928 __PACKAGE__-
>register_method({
4929 name
=> 'resize_vm',
4930 path
=> '{vmid}/resize',
4934 description
=> "Extend volume size.",
4936 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
4939 additionalProperties
=> 0,
4941 node
=> get_standard_option
('pve-node'),
4942 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4943 skiplock
=> get_standard_option
('skiplock'),
4946 description
=> "The disk you want to resize.",
4947 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
4951 pattern
=> '\+?\d+(\.\d+)?[KMGT]?',
4952 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.",
4956 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
4964 description
=> "the task ID.",
4969 my $rpcenv = PVE
::RPCEnvironment
::get
();
4971 my $authuser = $rpcenv->get_user();
4973 my $node = extract_param
($param, 'node');
4975 my $vmid = extract_param
($param, 'vmid');
4977 my $digest = extract_param
($param, 'digest');
4979 my $disk = extract_param
($param, 'disk');
4981 my $sizestr = extract_param
($param, 'size');
4983 my $skiplock = extract_param
($param, 'skiplock');
4984 raise_param_exc
({ skiplock
=> "Only root may use this option." })
4985 if $skiplock && $authuser ne 'root@pam';
4987 my $storecfg = PVE
::Storage
::config
();
4989 my $updatefn = sub {
4991 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4993 die "checksum missmatch (file change by other user?)\n"
4994 if $digest && $digest ne $conf->{digest
};
4995 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
4997 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4999 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
5001 my (undef, undef, undef, undef, undef, undef, $format) =
5002 PVE
::Storage
::parse_volname
($storecfg, $drive->{file
});
5004 my $volid = $drive->{file
};
5006 die "disk '$disk' has no associated volume\n" if !$volid;
5008 die "you can't resize a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive);
5010 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
5012 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
5014 PVE
::Storage
::activate_volumes
($storecfg, [$volid]);
5015 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid, 5);
5017 die "Could not determine current size of volume '$volid'\n" if !defined($size);
5019 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
5020 my ($ext, $newsize, $unit) = ($1, $2, $4);
5023 $newsize = $newsize * 1024;
5024 } elsif ($unit eq 'M') {
5025 $newsize = $newsize * 1024 * 1024;
5026 } elsif ($unit eq 'G') {
5027 $newsize = $newsize * 1024 * 1024 * 1024;
5028 } elsif ($unit eq 'T') {
5029 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
5032 $newsize += $size if $ext;
5033 $newsize = int($newsize);
5035 die "shrinking disks is not supported\n" if $newsize < $size;
5037 return if $size == $newsize;
5039 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
5041 PVE
::QemuServer
::qemu_block_resize
($vmid, "drive-$disk", $storecfg, $volid, $newsize);
5043 $drive->{size
} = $newsize;
5044 $conf->{$disk} = PVE
::QemuServer
::print_drive
($drive);
5046 PVE
::QemuConfig-
>write_config($vmid, $conf);
5050 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
5053 return $rpcenv->fork_worker('resize', $vmid, $authuser, $worker);
5056 __PACKAGE__-
>register_method({
5057 name
=> 'snapshot_list',
5058 path
=> '{vmid}/snapshot',
5060 description
=> "List all snapshots.",
5062 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5065 protected
=> 1, # qemu pid files are only readable by root
5067 additionalProperties
=> 0,
5069 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5070 node
=> get_standard_option
('pve-node'),
5079 description
=> "Snapshot identifier. Value 'current' identifies the current VM.",
5083 description
=> "Snapshot includes RAM.",
5088 description
=> "Snapshot description.",
5092 description
=> "Snapshot creation time",
5094 renderer
=> 'timestamp',
5098 description
=> "Parent snapshot identifier.",
5104 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
5109 my $vmid = $param->{vmid
};
5111 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5112 my $snaphash = $conf->{snapshots
} || {};
5116 foreach my $name (keys %$snaphash) {
5117 my $d = $snaphash->{$name};
5120 snaptime
=> $d->{snaptime
} || 0,
5121 vmstate
=> $d->{vmstate
} ?
1 : 0,
5122 description
=> $d->{description
} || '',
5124 $item->{parent
} = $d->{parent
} if $d->{parent
};
5125 $item->{snapstate
} = $d->{snapstate
} if $d->{snapstate
};
5129 my $running = PVE
::QemuServer
::check_running
($vmid, 1) ?
1 : 0;
5132 digest
=> $conf->{digest
},
5133 running
=> $running,
5134 description
=> "You are here!",
5136 $current->{parent
} = $conf->{parent
} if $conf->{parent
};
5138 push @$res, $current;
5143 __PACKAGE__-
>register_method({
5145 path
=> '{vmid}/snapshot',
5149 description
=> "Snapshot a VM.",
5151 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5154 additionalProperties
=> 0,
5156 node
=> get_standard_option
('pve-node'),
5157 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5158 snapname
=> get_standard_option
('pve-snapshot-name'),
5162 description
=> "Save the vmstate",
5167 description
=> "A textual description or comment.",
5173 description
=> "the task ID.",
5178 my $rpcenv = PVE
::RPCEnvironment
::get
();
5180 my $authuser = $rpcenv->get_user();
5182 my $node = extract_param
($param, 'node');
5184 my $vmid = extract_param
($param, 'vmid');
5186 my $snapname = extract_param
($param, 'snapname');
5188 die "unable to use snapshot name 'current' (reserved name)\n"
5189 if $snapname eq 'current';
5191 die "unable to use snapshot name 'pending' (reserved name)\n"
5192 if lc($snapname) eq 'pending';
5195 PVE
::Cluster
::log_msg
('info', $authuser, "snapshot VM $vmid: $snapname");
5196 PVE
::QemuConfig-
>snapshot_create($vmid, $snapname, $param->{vmstate
},
5197 $param->{description
});
5200 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
5203 __PACKAGE__-
>register_method({
5204 name
=> 'snapshot_cmd_idx',
5205 path
=> '{vmid}/snapshot/{snapname}',
5212 additionalProperties
=> 0,
5214 vmid
=> get_standard_option
('pve-vmid'),
5215 node
=> get_standard_option
('pve-node'),
5216 snapname
=> get_standard_option
('pve-snapshot-name'),
5225 links
=> [ { rel
=> 'child', href
=> "{cmd}" } ],
5232 push @$res, { cmd
=> 'rollback' };
5233 push @$res, { cmd
=> 'config' };
5238 __PACKAGE__-
>register_method({
5239 name
=> 'update_snapshot_config',
5240 path
=> '{vmid}/snapshot/{snapname}/config',
5244 description
=> "Update snapshot metadata.",
5246 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5249 additionalProperties
=> 0,
5251 node
=> get_standard_option
('pve-node'),
5252 vmid
=> get_standard_option
('pve-vmid'),
5253 snapname
=> get_standard_option
('pve-snapshot-name'),
5257 description
=> "A textual description or comment.",
5261 returns
=> { type
=> 'null' },
5265 my $rpcenv = PVE
::RPCEnvironment
::get
();
5267 my $authuser = $rpcenv->get_user();
5269 my $vmid = extract_param
($param, 'vmid');
5271 my $snapname = extract_param
($param, 'snapname');
5273 return if !defined($param->{description
});
5275 my $updatefn = sub {
5277 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5279 PVE
::QemuConfig-
>check_lock($conf);
5281 my $snap = $conf->{snapshots
}->{$snapname};
5283 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5285 $snap->{description
} = $param->{description
} if defined($param->{description
});
5287 PVE
::QemuConfig-
>write_config($vmid, $conf);
5290 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
5295 __PACKAGE__-
>register_method({
5296 name
=> 'get_snapshot_config',
5297 path
=> '{vmid}/snapshot/{snapname}/config',
5300 description
=> "Get snapshot configuration",
5302 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback', 'VM.Audit' ], any
=> 1],
5305 additionalProperties
=> 0,
5307 node
=> get_standard_option
('pve-node'),
5308 vmid
=> get_standard_option
('pve-vmid'),
5309 snapname
=> get_standard_option
('pve-snapshot-name'),
5312 returns
=> { type
=> "object" },
5316 my $rpcenv = PVE
::RPCEnvironment
::get
();
5318 my $authuser = $rpcenv->get_user();
5320 my $vmid = extract_param
($param, 'vmid');
5322 my $snapname = extract_param
($param, 'snapname');
5324 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5326 my $snap = $conf->{snapshots
}->{$snapname};
5328 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5333 __PACKAGE__-
>register_method({
5335 path
=> '{vmid}/snapshot/{snapname}/rollback',
5339 description
=> "Rollback VM state to specified snapshot.",
5341 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any
=> 1],
5344 additionalProperties
=> 0,
5346 node
=> get_standard_option
('pve-node'),
5347 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5348 snapname
=> get_standard_option
('pve-snapshot-name'),
5351 description
=> "Whether the VM should get started after rolling back successfully."
5352 . " (Note: VMs will be automatically started if the snapshot includes RAM.)",
5360 description
=> "the task ID.",
5365 my $rpcenv = PVE
::RPCEnvironment
::get
();
5367 my $authuser = $rpcenv->get_user();
5369 my $node = extract_param
($param, 'node');
5371 my $vmid = extract_param
($param, 'vmid');
5373 my $snapname = extract_param
($param, 'snapname');
5376 PVE
::Cluster
::log_msg
('info', $authuser, "rollback snapshot VM $vmid: $snapname");
5377 PVE
::QemuConfig-
>snapshot_rollback($vmid, $snapname);
5379 if ($param->{start
} && !PVE
::QemuServer
::Helpers
::vm_running_locally
($vmid)) {
5380 PVE
::API2
::Qemu-
>vm_start({ vmid
=> $vmid, node
=> $node });
5385 # hold migration lock, this makes sure that nobody create replication snapshots
5386 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
5389 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
5392 __PACKAGE__-
>register_method({
5393 name
=> 'delsnapshot',
5394 path
=> '{vmid}/snapshot/{snapname}',
5398 description
=> "Delete a VM snapshot.",
5400 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5403 additionalProperties
=> 0,
5405 node
=> get_standard_option
('pve-node'),
5406 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5407 snapname
=> get_standard_option
('pve-snapshot-name'),
5411 description
=> "For removal from config file, even if removing disk snapshots fails.",
5417 description
=> "the task ID.",
5422 my $rpcenv = PVE
::RPCEnvironment
::get
();
5424 my $authuser = $rpcenv->get_user();
5426 my $node = extract_param
($param, 'node');
5428 my $vmid = extract_param
($param, 'vmid');
5430 my $snapname = extract_param
($param, 'snapname');
5433 my $do_delete = sub {
5435 PVE
::Cluster
::log_msg
('info', $authuser, "delete snapshot VM $vmid: $snapname");
5436 PVE
::QemuConfig-
>snapshot_delete($vmid, $snapname, $param->{force
});
5440 if ($param->{force
}) {
5443 eval { PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $do_delete); };
5445 die $err if $lock_obtained;
5446 die "Failed to obtain guest migration lock - replication running?\n";
5451 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
5454 __PACKAGE__-
>register_method({
5456 path
=> '{vmid}/template',
5460 description
=> "Create a Template.",
5462 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid}",
5463 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
5466 additionalProperties
=> 0,
5468 node
=> get_standard_option
('pve-node'),
5469 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
5473 description
=> "If you want to convert only 1 disk to base image.",
5474 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
5481 description
=> "the task ID.",
5486 my $rpcenv = PVE
::RPCEnvironment
::get
();
5488 my $authuser = $rpcenv->get_user();
5490 my $node = extract_param
($param, 'node');
5492 my $vmid = extract_param
($param, 'vmid');
5494 my $disk = extract_param
($param, 'disk');
5496 my $load_and_check = sub {
5497 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5499 PVE
::QemuConfig-
>check_lock($conf);
5501 die "unable to create template, because VM contains snapshots\n"
5502 if $conf->{snapshots
} && scalar(keys %{$conf->{snapshots
}});
5504 die "you can't convert a template to a template\n"
5505 if PVE
::QemuConfig-
>is_template($conf) && !$disk;
5507 die "you can't convert a VM to template if VM is running\n"
5508 if PVE
::QemuServer
::check_running
($vmid);
5513 $load_and_check->();
5516 PVE
::QemuConfig-
>lock_config($vmid, sub {
5517 my $conf = $load_and_check->();
5519 $conf->{template
} = 1;
5520 PVE
::QemuConfig-
>write_config($vmid, $conf);
5522 PVE
::QemuServer
::template_create
($vmid, $conf, $disk);
5526 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
5529 __PACKAGE__-
>register_method({
5530 name
=> 'cloudinit_generated_config_dump',
5531 path
=> '{vmid}/cloudinit/dump',
5534 description
=> "Get automatically generated cloudinit config.",
5536 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5539 additionalProperties
=> 0,
5541 node
=> get_standard_option
('pve-node'),
5542 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5544 description
=> 'Config type.',
5546 enum
=> ['user', 'network', 'meta'],
5556 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
5558 return PVE
::QemuServer
::Cloudinit
::dump_cloudinit_config
($conf, $param->{vmid
}, $param->{type
});
5561 __PACKAGE__-
>register_method({
5563 path
=> '{vmid}/mtunnel',
5566 description
=> 'Migration tunnel endpoint - only for internal use by VM migration.',
5570 ['perm', '/vms/{vmid}', [ 'VM.Allocate' ]],
5571 ['perm', '/', [ 'Sys.Incoming' ]],
5573 description
=> "You need 'VM.Allocate' permissions on '/vms/{vmid}' and Sys.Incoming" .
5574 " on '/'. Further permission checks happen during the actual migration.",
5577 additionalProperties
=> 0,
5579 node
=> get_standard_option
('pve-node'),
5580 vmid
=> get_standard_option
('pve-vmid'),
5583 format
=> 'pve-storage-id-list',
5585 description
=> 'List of storages to check permission and availability. Will be checked again for all actually used storages during migration.',
5589 format
=> 'pve-bridge-id-list',
5591 description
=> 'List of network bridges to check availability. Will be checked again for actually used bridges during migration.',
5596 additionalProperties
=> 0,
5598 upid
=> { type
=> 'string' },
5599 ticket
=> { type
=> 'string' },
5600 socket => { type
=> 'string' },
5606 my $rpcenv = PVE
::RPCEnvironment
::get
();
5607 my $authuser = $rpcenv->get_user();
5609 my $node = extract_param
($param, 'node');
5610 my $vmid = extract_param
($param, 'vmid');
5612 my $storages = extract_param
($param, 'storages');
5613 my $bridges = extract_param
($param, 'bridges');
5615 my $nodename = PVE
::INotify
::nodename
();
5617 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
5618 if $node ne 'localhost' && $node ne $nodename;
5622 my $storecfg = PVE
::Storage
::config
();
5623 foreach my $storeid (PVE
::Tools
::split_list
($storages)) {
5624 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storeid, $node);
5627 foreach my $bridge (PVE
::Tools
::split_list
($bridges)) {
5628 PVE
::Network
::read_bridge_mtu
($bridge);
5631 PVE
::Cluster
::check_cfs_quorum
();
5633 my $lock = 'create';
5634 eval { PVE
::QemuConfig-
>create_and_lock_config($vmid, 0, $lock); };
5636 raise_param_exc
({ vmid
=> "unable to create empty VM config - $@"})
5641 storecfg
=> PVE
::Storage
::config
(),
5646 my $run_locked = sub {
5647 my ($code, $params) = @_;
5648 return PVE
::QemuConfig-
>lock_config($state->{vmid
}, sub {
5649 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5651 $state->{conf
} = $conf;
5653 die "Encountered wrong lock - aborting mtunnel command handling.\n"
5654 if $state->{lock} && !PVE
::QemuConfig-
>has_lock($conf, $state->{lock});
5656 return $code->($params);
5664 description
=> 'Full VM config, adapted for target cluster/node',
5666 'firewall-config' => {
5668 description
=> 'VM firewall config',
5673 format
=> PVE
::JSONSchema
::get_standard_option
('pve-qm-image-format'),
5676 format
=> 'pve-storage-id',
5680 description
=> 'parsed drive information without volid and format',
5686 description
=> 'params passed to vm_start_nolock',
5690 description
=> 'migrate_opts passed to vm_start_nolock',
5696 description
=> 'socket path for which the ticket should be valid. must be known to current mtunnel instance.',
5702 description
=> 'remove VM config and disks, aborting migration',
5706 'disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'disk-import'},
5707 'query-disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'query-disk-import'},
5708 bwlimit
=> $PVE::StorageTunnel
::cmd_schema-
>{bwlimit
},
5711 my $cmd_handlers = {
5713 # compared against other end's version
5714 # bump/reset for breaking changes
5715 # bump/bump for opt-in changes
5717 api
=> $PVE::QemuMigrate
::WS_TUNNEL_VERSION
,
5724 # parse and write out VM FW config if given
5725 if (my $fw_conf = $params->{'firewall-config'}) {
5726 my ($path, $fh) = PVE
::Tools
::tempfile_contents
($fw_conf, 700);
5733 ipset_comments
=> {},
5735 my $cluster_fw_conf = PVE
::Firewall
::load_clusterfw_conf
();
5737 # TODO: add flag for strict parsing?
5738 # TODO: add import sub that does all this given raw content?
5739 my $vmfw_conf = PVE
::Firewall
::generic_fw_config_parser
($path, $cluster_fw_conf, $empty_conf, 'vm');
5740 $vmfw_conf->{vmid
} = $state->{vmid
};
5741 PVE
::Firewall
::save_vmfw_conf
($state->{vmid
}, $vmfw_conf);
5743 $state->{cleanup
}->{fw
} = 1;
5746 my $conf_fn = "incoming/qemu-server/$state->{vmid}.conf";
5747 my $new_conf = PVE
::QemuServer
::parse_vm_config
($conf_fn, $params->{conf
}, 1);
5748 delete $new_conf->{lock};
5749 delete $new_conf->{digest
};
5751 # TODO handle properly?
5752 delete $new_conf->{snapshots
};
5753 delete $new_conf->{parent
};
5754 delete $new_conf->{pending
};
5756 # not handled by update_vm_api
5757 my $vmgenid = delete $new_conf->{vmgenid
};
5758 my $meta = delete $new_conf->{meta
};
5759 my $cloudinit = delete $new_conf->{cloudinit
}; # this is informational only
5760 $new_conf->{skip_cloud_init
} = 1; # re-use image from source side
5762 $new_conf->{vmid
} = $state->{vmid
};
5763 $new_conf->{node
} = $node;
5765 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, 'create');
5768 $update_vm_api->($new_conf, 1);
5771 # revert to locked previous config
5772 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5773 $conf->{lock} = 'create';
5774 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5779 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5780 $conf->{lock} = 'migrate';
5781 $conf->{vmgenid
} = $vmgenid if defined($vmgenid);
5782 $conf->{meta
} = $meta if defined($meta);
5783 $conf->{cloudinit
} = $cloudinit if defined($cloudinit);
5784 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5786 $state->{lock} = 'migrate';
5792 return PVE
::StorageTunnel
::handle_bwlimit
($params);
5797 my $format = $params->{format
};
5798 my $storeid = $params->{storage
};
5799 my $drive = $params->{drive
};
5801 $check_storage_access_migrate->($rpcenv, $authuser, $state->{storecfg
}, $storeid, $node);
5804 default => $storeid,
5807 my $source_volumes = {
5817 my $res = PVE
::QemuServer
::vm_migrate_alloc_nbd_disks
($state->{storecfg
}, $state->{vmid
}, $source_volumes, $storagemap);
5818 if (defined($res->{disk
})) {
5819 $state->{cleanup
}->{volumes
}->{$res->{disk
}->{volid
}} = 1;
5820 return $res->{disk
};
5822 die "failed to allocate NBD disk..\n";
5825 'disk-import' => sub {
5828 $check_storage_access_migrate->(
5836 $params->{unix
} = "/run/qemu-server/$state->{vmid}.storage";
5838 return PVE
::StorageTunnel
::handle_disk_import
($state, $params);
5840 'query-disk-import' => sub {
5843 return PVE
::StorageTunnel
::handle_query_disk_import
($state, $params);
5848 my $info = PVE
::QemuServer
::vm_start_nolock
(
5852 $params->{start_params
},
5853 $params->{migrate_opts
},
5857 if ($info->{migrate
}->{proto
} ne 'unix') {
5858 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5859 die "migration over non-UNIX sockets not possible\n";
5862 my $socket = $info->{migrate
}->{addr
};
5863 chown $state->{socket_uid
}, -1, $socket;
5864 $state->{sockets
}->{$socket} = 1;
5866 my $unix_sockets = $info->{migrate
}->{unix_sockets
};
5867 foreach my $socket (@$unix_sockets) {
5868 chown $state->{socket_uid
}, -1, $socket;
5869 $state->{sockets
}->{$socket} = 1;
5874 if (PVE
::QemuServer
::qga_check_running
($state->{vmid
})) {
5875 eval { mon_cmd
($state->{vmid
}, "guest-fstrim") };
5876 warn "fstrim failed: $@\n" if $@;
5881 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5885 PVE
::QemuServer
::nbd_stop
($state->{vmid
});
5889 if (PVE
::QemuServer
::Helpers
::vm_running_locally
($state->{vmid
})) {
5890 PVE
::QemuServer
::vm_resume
($state->{vmid
}, 1, 1);
5892 die "VM $state->{vmid} not running\n";
5897 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, $state->{lock});
5898 delete $state->{lock};
5904 my $path = $params->{path
};
5906 die "Not allowed to generate ticket for unknown socket '$path'\n"
5907 if !defined($state->{sockets
}->{$path});
5909 return { ticket
=> PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$path") };
5914 if ($params->{cleanup
}) {
5915 if ($state->{cleanup
}->{fw
}) {
5916 PVE
::Firewall
::remove_vmfw_conf
($state->{vmid
});
5919 for my $volid (keys $state->{cleanup
}->{volumes
}->%*) {
5920 print "freeing volume '$volid' as part of cleanup\n";
5921 eval { PVE
::Storage
::vdisk_free
($state->{storecfg
}, $volid) };
5925 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5928 print "switching to exit-mode, waiting for client to disconnect\n";
5935 my $socket_addr = "/run/qemu-server/$state->{vmid}.mtunnel";
5936 unlink $socket_addr;
5938 $state->{socket} = IO
::Socket
::UNIX-
>new(
5939 Type
=> SOCK_STREAM
(),
5940 Local
=> $socket_addr,
5944 $state->{socket_uid
} = getpwnam('www-data')
5945 or die "Failed to resolve user 'www-data' to numeric UID\n";
5946 chown $state->{socket_uid
}, -1, $socket_addr;
5949 print "mtunnel started\n";
5951 my $conn = eval { PVE
::Tools
::run_with_timeout
(300, sub { $state->{socket}->accept() }) };
5953 warn "Failed to accept tunnel connection - $@\n";
5955 warn "Removing tunnel socket..\n";
5956 unlink $state->{socket};
5958 warn "Removing temporary VM config..\n";
5960 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5963 die "Exiting mtunnel\n";
5966 $state->{conn
} = $conn;
5968 my $reply_err = sub {
5971 my $reply = JSON
::encode_json
({
5972 success
=> JSON
::false
,
5975 $conn->print("$reply\n");
5979 my $reply_ok = sub {
5982 $res->{success
} = JSON
::true
;
5983 my $reply = JSON
::encode_json
($res);
5984 $conn->print("$reply\n");
5988 while (my $line = <$conn>) {
5991 # untaint, we validate below if needed
5992 ($line) = $line =~ /^(.*)$/;
5993 my $parsed = eval { JSON
::decode_json
($line) };
5995 $reply_err->("failed to parse command - $@");
5999 my $cmd = delete $parsed->{cmd
};
6000 if (!defined($cmd)) {
6001 $reply_err->("'cmd' missing");
6002 } elsif ($state->{exit}) {
6003 $reply_err->("tunnel is in exit-mode, processing '$cmd' cmd not possible");
6005 } elsif (my $handler = $cmd_handlers->{$cmd}) {
6006 print "received command '$cmd'\n";
6008 if ($cmd_desc->{$cmd}) {
6009 PVE
::JSONSchema
::validate
($parsed, $cmd_desc->{$cmd});
6013 my $res = $run_locked->($handler, $parsed);
6016 $reply_err->("failed to handle '$cmd' command - $@")
6019 $reply_err->("unknown command '$cmd' given");
6023 if ($state->{exit}) {
6024 print "mtunnel exited\n";
6026 die "mtunnel exited unexpectedly\n";
6030 my $socket_addr = "/run/qemu-server/$vmid.mtunnel";
6031 my $ticket = PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$socket_addr");
6032 my $upid = $rpcenv->fork_worker('qmtunnel', $vmid, $authuser, $realcmd);
6037 socket => $socket_addr,
6041 __PACKAGE__-
>register_method({
6042 name
=> 'mtunnelwebsocket',
6043 path
=> '{vmid}/mtunnelwebsocket',
6046 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.",
6047 user
=> 'all', # check inside
6049 description
=> 'Migration tunnel endpoint for websocket upgrade - only for internal use by VM migration.',
6051 additionalProperties
=> 0,
6053 node
=> get_standard_option
('pve-node'),
6054 vmid
=> get_standard_option
('pve-vmid'),
6057 description
=> "unix socket to forward to",
6061 description
=> "ticket return by initial 'mtunnel' API call, or retrieved via 'ticket' tunnel command",
6068 port
=> { type
=> 'string', optional
=> 1 },
6069 socket => { type
=> 'string', optional
=> 1 },
6075 my $rpcenv = PVE
::RPCEnvironment
::get
();
6076 my $authuser = $rpcenv->get_user();
6078 my $nodename = PVE
::INotify
::nodename
();
6079 my $node = extract_param
($param, 'node');
6081 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
6082 if $node ne 'localhost' && $node ne $nodename;
6084 my $vmid = $param->{vmid
};
6086 PVE
::QemuConfig-
>load_config($vmid);
6088 my $socket = $param->{socket};
6089 PVE
::AccessControl
::verify_tunnel_ticket
($param->{ticket
}, $authuser, "/socket/$socket");
6091 return { socket => $socket };