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
=> "Try to abort active '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"
3177 ." physical machine. This will send an ACPI event for the guest OS, which should then"
3178 ." proceed to a clean shutdown.",
3180 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3183 additionalProperties
=> 0,
3185 node
=> get_standard_option
('pve-node'),
3186 vmid
=> get_standard_option
('pve-vmid',
3187 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3188 skiplock
=> get_standard_option
('skiplock'),
3190 description
=> "Wait maximal timeout seconds.",
3196 description
=> "Make sure the VM stops.",
3202 description
=> "Do not deactivate storage volumes.",
3215 my $rpcenv = PVE
::RPCEnvironment
::get
();
3216 my $authuser = $rpcenv->get_user();
3218 my $node = extract_param
($param, 'node');
3219 my $vmid = extract_param
($param, 'vmid');
3221 my $skiplock = extract_param
($param, 'skiplock');
3222 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3223 if $skiplock && $authuser ne 'root@pam';
3225 my $keepActive = extract_param
($param, 'keepActive');
3226 raise_param_exc
({ keepActive
=> "Only root may use this option." })
3227 if $keepActive && $authuser ne 'root@pam';
3229 my $storecfg = PVE
::Storage
::config
();
3233 # sending a graceful shutdown command to paused VMs runs into timeouts, and even worse, when
3234 # the VM gets resumed later, it still gets the request delivered and powers off
3235 if (PVE
::QemuServer
::vm_is_paused
($vmid, 1)) {
3236 if ($param->{forceStop
}) {
3237 warn "VM is paused - stop instead of shutdown\n";
3240 die "VM is paused - cannot shutdown\n";
3244 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
3246 my $timeout = $param->{timeout
} // 60;
3250 print "Requesting HA stop for VM $vmid\n";
3252 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", "$timeout"];
3253 PVE
::Tools
::run_command
($cmd);
3257 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
3264 syslog
('info', "shutdown VM $vmid: $upid\n");
3266 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0, $param->{timeout
},
3267 $shutdown, $param->{forceStop
}, $keepActive);
3271 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
3275 __PACKAGE__-
>register_method({
3276 name
=> 'vm_reboot',
3277 path
=> '{vmid}/status/reboot',
3281 description
=> "Reboot the VM by shutting it down, and starting it again. Applies pending changes.",
3283 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3286 additionalProperties
=> 0,
3288 node
=> get_standard_option
('pve-node'),
3289 vmid
=> get_standard_option
('pve-vmid',
3290 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3292 description
=> "Wait maximal timeout seconds for the shutdown.",
3305 my $rpcenv = PVE
::RPCEnvironment
::get
();
3306 my $authuser = $rpcenv->get_user();
3308 my $node = extract_param
($param, 'node');
3309 my $vmid = extract_param
($param, 'vmid');
3311 die "VM is paused - cannot shutdown\n" if PVE
::QemuServer
::vm_is_paused
($vmid, 1);
3313 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3318 syslog
('info', "requesting reboot of VM $vmid: $upid\n");
3319 PVE
::QemuServer
::vm_reboot
($vmid, $param->{timeout
});
3323 return $rpcenv->fork_worker('qmreboot', $vmid, $authuser, $realcmd);
3326 __PACKAGE__-
>register_method({
3327 name
=> 'vm_suspend',
3328 path
=> '{vmid}/status/suspend',
3332 description
=> "Suspend virtual machine.",
3334 description
=> "You need 'VM.PowerMgmt' on /vms/{vmid}, and if you have set 'todisk',".
3335 " you need also 'VM.Config.Disk' on /vms/{vmid} and 'Datastore.AllocateSpace'".
3336 " on the storage for the vmstate.",
3337 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3340 additionalProperties
=> 0,
3342 node
=> get_standard_option
('pve-node'),
3343 vmid
=> get_standard_option
('pve-vmid',
3344 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3345 skiplock
=> get_standard_option
('skiplock'),
3350 description
=> 'If set, suspends the VM to disk. Will be resumed on next VM start.',
3352 statestorage
=> get_standard_option
('pve-storage-id', {
3353 description
=> "The storage for the VM state",
3354 requires
=> 'todisk',
3356 completion
=> \
&PVE
::Storage
::complete_storage_enabled
,
3366 my $rpcenv = PVE
::RPCEnvironment
::get
();
3367 my $authuser = $rpcenv->get_user();
3369 my $node = extract_param
($param, 'node');
3370 my $vmid = extract_param
($param, 'vmid');
3372 my $todisk = extract_param
($param, 'todisk') // 0;
3374 my $statestorage = extract_param
($param, 'statestorage');
3376 my $skiplock = extract_param
($param, 'skiplock');
3377 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3378 if $skiplock && $authuser ne 'root@pam';
3380 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3382 die "Cannot suspend HA managed VM to disk\n"
3383 if $todisk && PVE
::HA
::Config
::vm_is_ha_managed
($vmid);
3385 # early check for storage permission, for better user feedback
3387 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
3388 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3390 # cannot save the state of a non-virtualized PCIe device, so resume cannot really work
3391 for my $key (keys %$conf) {
3392 next if $key !~ /^hostpci\d+/;
3393 die "cannot suspend VM to disk due to passed-through PCI device(s), which lack the"
3394 ." possibility to save/restore their internal state\n";
3397 if (!$statestorage) {
3398 # get statestorage from config if none is given
3399 my $storecfg = PVE
::Storage
::config
();
3400 $statestorage = PVE
::QemuServer
::find_vmstate_storage
($conf, $storecfg);
3403 $rpcenv->check($authuser, "/storage/$statestorage", ['Datastore.AllocateSpace']);
3409 syslog
('info', "suspend VM $vmid: $upid\n");
3411 PVE
::QemuServer
::vm_suspend
($vmid, $skiplock, $todisk, $statestorage);
3416 my $taskname = $todisk ?
'qmsuspend' : 'qmpause';
3417 return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd);
3420 __PACKAGE__-
>register_method({
3421 name
=> 'vm_resume',
3422 path
=> '{vmid}/status/resume',
3426 description
=> "Resume virtual machine.",
3428 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3431 additionalProperties
=> 0,
3433 node
=> get_standard_option
('pve-node'),
3434 vmid
=> get_standard_option
('pve-vmid',
3435 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3436 skiplock
=> get_standard_option
('skiplock'),
3437 nocheck
=> { type
=> 'boolean', optional
=> 1 },
3447 my $rpcenv = PVE
::RPCEnvironment
::get
();
3449 my $authuser = $rpcenv->get_user();
3451 my $node = extract_param
($param, 'node');
3453 my $vmid = extract_param
($param, 'vmid');
3455 my $skiplock = extract_param
($param, 'skiplock');
3456 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3457 if $skiplock && $authuser ne 'root@pam';
3459 # nocheck is used as part of migration when config file might be still
3461 my $nocheck = extract_param
($param, 'nocheck');
3462 raise_param_exc
({ nocheck
=> "Only root may use this option." })
3463 if $nocheck && $authuser ne 'root@pam';
3465 my $to_disk_suspended;
3467 PVE
::QemuConfig-
>lock_config($vmid, sub {
3468 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3469 $to_disk_suspended = PVE
::QemuConfig-
>has_lock($conf, 'suspended');
3473 die "VM $vmid not running\n"
3474 if !$to_disk_suspended && !PVE
::QemuServer
::check_running
($vmid, $nocheck);
3479 syslog
('info', "resume VM $vmid: $upid\n");
3481 if (!$to_disk_suspended) {
3482 PVE
::QemuServer
::vm_resume
($vmid, $skiplock, $nocheck);
3484 my $storecfg = PVE
::Storage
::config
();
3485 PVE
::QemuServer
::vm_start
($storecfg, $vmid, { skiplock
=> $skiplock });
3491 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
3494 __PACKAGE__-
>register_method({
3495 name
=> 'vm_sendkey',
3496 path
=> '{vmid}/sendkey',
3500 description
=> "Send key event to virtual machine.",
3502 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
3505 additionalProperties
=> 0,
3507 node
=> get_standard_option
('pve-node'),
3508 vmid
=> get_standard_option
('pve-vmid',
3509 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3510 skiplock
=> get_standard_option
('skiplock'),
3512 description
=> "The key (qemu monitor encoding).",
3517 returns
=> { type
=> 'null'},
3521 my $rpcenv = PVE
::RPCEnvironment
::get
();
3523 my $authuser = $rpcenv->get_user();
3525 my $node = extract_param
($param, 'node');
3527 my $vmid = extract_param
($param, 'vmid');
3529 my $skiplock = extract_param
($param, 'skiplock');
3530 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3531 if $skiplock && $authuser ne 'root@pam';
3533 PVE
::QemuServer
::vm_sendkey
($vmid, $skiplock, $param->{key
});
3538 __PACKAGE__-
>register_method({
3539 name
=> 'vm_feature',
3540 path
=> '{vmid}/feature',
3544 description
=> "Check if feature for virtual machine is available.",
3546 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3549 additionalProperties
=> 0,
3551 node
=> get_standard_option
('pve-node'),
3552 vmid
=> get_standard_option
('pve-vmid'),
3554 description
=> "Feature to check.",
3556 enum
=> [ 'snapshot', 'clone', 'copy' ],
3558 snapname
=> get_standard_option
('pve-snapshot-name', {
3566 hasFeature
=> { type
=> 'boolean' },
3569 items
=> { type
=> 'string' },
3576 my $node = extract_param
($param, 'node');
3578 my $vmid = extract_param
($param, 'vmid');
3580 my $snapname = extract_param
($param, 'snapname');
3582 my $feature = extract_param
($param, 'feature');
3584 my $running = PVE
::QemuServer
::check_running
($vmid);
3586 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3589 my $snap = $conf->{snapshots
}->{$snapname};
3590 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3593 my $storecfg = PVE
::Storage
::config
();
3595 my $nodelist = PVE
::QemuServer
::shared_nodes
($conf, $storecfg);
3596 my $hasFeature = PVE
::QemuConfig-
>has_feature($feature, $conf, $storecfg, $snapname, $running);
3599 hasFeature
=> $hasFeature,
3600 nodes
=> [ keys %$nodelist ],
3604 __PACKAGE__-
>register_method({
3606 path
=> '{vmid}/clone',
3610 description
=> "Create a copy of virtual machine/template.",
3612 description
=> "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
3613 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
3614 "'Datastore.AllocateSpace' on any used storage and 'SDN.Use' on any used bridge/vnet",
3617 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
3619 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
3620 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param
=> 'pool'],
3625 additionalProperties
=> 0,
3627 node
=> get_standard_option
('pve-node'),
3628 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3629 newid
=> get_standard_option
('pve-vmid', {
3630 completion
=> \
&PVE
::Cluster
::complete_next_vmid
,
3631 description
=> 'VMID for the clone.' }),
3634 type
=> 'string', format
=> 'dns-name',
3635 description
=> "Set a name for the new VM.",
3640 description
=> "Description for the new VM.",
3644 type
=> 'string', format
=> 'pve-poolid',
3645 description
=> "Add the new VM to the specified pool.",
3647 snapname
=> get_standard_option
('pve-snapshot-name', {
3650 storage
=> get_standard_option
('pve-storage-id', {
3651 description
=> "Target storage for full clone.",
3655 description
=> "Target format for file storage. Only valid for full clone.",
3658 enum
=> [ 'raw', 'qcow2', 'vmdk'],
3663 description
=> "Create a full copy of all disks. This is always done when " .
3664 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
3666 target
=> get_standard_option
('pve-node', {
3667 description
=> "Target node. Only allowed if the original VM is on shared storage.",
3671 description
=> "Override I/O bandwidth limit (in KiB/s).",
3675 default => 'clone limit from datacenter or storage config',
3685 my $rpcenv = PVE
::RPCEnvironment
::get
();
3686 my $authuser = $rpcenv->get_user();
3688 my $node = extract_param
($param, 'node');
3689 my $vmid = extract_param
($param, 'vmid');
3690 my $newid = extract_param
($param, 'newid');
3691 my $pool = extract_param
($param, 'pool');
3693 my $snapname = extract_param
($param, 'snapname');
3694 my $storage = extract_param
($param, 'storage');
3695 my $format = extract_param
($param, 'format');
3696 my $target = extract_param
($param, 'target');
3698 my $localnode = PVE
::INotify
::nodename
();
3700 if ($target && ($target eq $localnode || $target eq 'localhost')) {
3704 my $running = PVE
::QemuServer
::check_running
($vmid) || 0;
3706 my $load_and_check = sub {
3707 $rpcenv->check_pool_exist($pool) if defined($pool);
3708 PVE
::Cluster
::check_node_exists
($target) if $target;
3710 my $storecfg = PVE
::Storage
::config
();
3713 # check if storage is enabled on local node
3714 PVE
::Storage
::storage_check_enabled
($storecfg, $storage);
3716 # check if storage is available on target node
3717 PVE
::Storage
::storage_check_enabled
($storecfg, $storage, $target);
3718 # clone only works if target storage is shared
3719 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storage);
3720 die "can't clone to non-shared storage '$storage'\n"
3721 if !$scfg->{shared
};
3725 PVE
::Cluster
::check_cfs_quorum
();
3727 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3728 PVE
::QemuConfig-
>check_lock($conf);
3730 my $verify_running = PVE
::QemuServer
::check_running
($vmid) || 0;
3731 die "unexpected state change\n" if $verify_running != $running;
3733 die "snapshot '$snapname' does not exist\n"
3734 if $snapname && !defined( $conf->{snapshots
}->{$snapname});
3736 my $full = $param->{full
} // !PVE
::QemuConfig-
>is_template($conf);
3738 die "parameter 'storage' not allowed for linked clones\n"
3739 if defined($storage) && !$full;
3741 die "parameter 'format' not allowed for linked clones\n"
3742 if defined($format) && !$full;
3744 my $oldconf = $snapname ?
$conf->{snapshots
}->{$snapname} : $conf;
3746 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
3747 PVE
::QemuServer
::check_mapping_access
($rpcenv, $authuser, $oldconf);
3749 PVE
::QemuServer
::check_bridge_access
($rpcenv, $authuser, $oldconf);
3751 die "can't clone VM to node '$target' (VM uses local storage)\n"
3752 if $target && !$sharedvm;
3754 my $conffile = PVE
::QemuConfig-
>config_file($newid);
3755 die "unable to create VM $newid: config file already exists\n"
3758 my $newconf = { lock => 'clone' };
3763 foreach my $opt (keys %$oldconf) {
3764 my $value = $oldconf->{$opt};
3766 # do not copy snapshot related info
3767 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
3768 $opt eq 'vmstate' || $opt eq 'snapstate';
3770 # no need to copy unused images, because VMID(owner) changes anyways
3771 next if $opt =~ m/^unused\d+$/;
3773 die "cannot clone TPM state while VM is running\n"
3774 if $full && $running && !$snapname && $opt eq 'tpmstate0';
3776 # always change MAC! address
3777 if ($opt =~ m/^net(\d+)$/) {
3778 my $net = PVE
::QemuServer
::parse_net
($value);
3779 my $dc = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
3780 $net->{macaddr
} = PVE
::Tools
::random_ether_addr
($dc->{mac_prefix
});
3781 $newconf->{$opt} = PVE
::QemuServer
::print_net
($net);
3782 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
3783 my $drive = PVE
::QemuServer
::parse_drive
($opt, $value);
3784 die "unable to parse drive options for '$opt'\n" if !$drive;
3785 if (PVE
::QemuServer
::drive_is_cdrom
($drive, 1)) {
3786 $newconf->{$opt} = $value; # simply copy configuration
3788 if ($full || PVE
::QemuServer
::drive_is_cloudinit
($drive)) {
3789 die "Full clone feature is not supported for drive '$opt'\n"
3790 if !PVE
::Storage
::volume_has_feature
($storecfg, 'copy', $drive->{file
}, $snapname, $running);
3791 $fullclone->{$opt} = 1;
3793 # not full means clone instead of copy
3794 die "Linked clone feature is not supported for drive '$opt'\n"
3795 if !PVE
::Storage
::volume_has_feature
($storecfg, 'clone', $drive->{file
}, $snapname, $running);
3797 $drives->{$opt} = $drive;
3798 next if PVE
::QemuServer
::drive_is_cloudinit
($drive);
3799 push @$vollist, $drive->{file
};
3802 # copy everything else
3803 $newconf->{$opt} = $value;
3807 return ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone);
3811 my ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone) = $load_and_check->();
3812 my $storecfg = PVE
::Storage
::config
();
3814 # auto generate a new uuid
3815 my $smbios1 = PVE
::QemuServer
::parse_smbios1
($newconf->{smbios1
} || '');
3816 $smbios1->{uuid
} = PVE
::QemuServer
::generate_uuid
();
3817 $newconf->{smbios1
} = PVE
::QemuServer
::print_smbios1
($smbios1);
3818 # auto generate a new vmgenid only if the option was set for template
3819 if ($newconf->{vmgenid
}) {
3820 $newconf->{vmgenid
} = PVE
::QemuServer
::generate_uuid
();
3823 delete $newconf->{template
};
3825 if ($param->{name
}) {
3826 $newconf->{name
} = $param->{name
};
3828 $newconf->{name
} = "Copy-of-VM-" . ($oldconf->{name
} // $vmid);
3831 if ($param->{description
}) {
3832 $newconf->{description
} = $param->{description
};
3835 # create empty/temp config - this fails if VM already exists on other node
3836 # FIXME use PVE::QemuConfig->create_and_lock_config and adapt code
3837 PVE
::Tools
::file_set_contents
($conffile, "# qmclone temporary file\nlock: clone\n");
3839 PVE
::Firewall
::clone_vmfw_conf
($vmid, $newid);
3841 my $newvollist = [];
3848 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
3850 PVE
::Storage
::activate_volumes
($storecfg, $vollist, $snapname);
3852 my $bwlimit = extract_param
($param, 'bwlimit');
3854 my $total_jobs = scalar(keys %{$drives});
3857 foreach my $opt (sort keys %$drives) {
3858 my $drive = $drives->{$opt};
3859 my $skipcomplete = ($total_jobs != $i); # finish after last drive
3860 my $completion = $skipcomplete ?
'skip' : 'complete';
3862 my $src_sid = PVE
::Storage
::parse_volume_id
($drive->{file
});
3863 my $storage_list = [ $src_sid ];
3864 push @$storage_list, $storage if defined($storage);
3865 my $clonelimit = PVE
::Storage
::get_bandwidth_limit
('clone', $storage_list, $bwlimit);
3869 running
=> $running,
3872 snapname
=> $snapname,
3878 storage
=> $storage,
3882 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($oldconf)
3883 if $opt eq 'efidisk0';
3885 my $newdrive = PVE
::QemuServer
::clone_disk
(
3897 $newconf->{$opt} = PVE
::QemuServer
::print_drive
($newdrive);
3899 PVE
::QemuConfig-
>write_config($newid, $newconf);
3903 delete $newconf->{lock};
3905 # do not write pending changes
3906 if (my @changes = keys %{$newconf->{pending
}}) {
3907 my $pending = join(',', @changes);
3908 warn "found pending changes for '$pending', discarding for clone\n";
3909 delete $newconf->{pending
};
3912 PVE
::QemuConfig-
>write_config($newid, $newconf);
3914 PVE
::QemuServer
::create_ifaces_ipams_ips
($newconf, $newid);
3918 # always deactivate volumes – avoids that LVM LVs are active on several nodes
3919 eval { PVE
::Storage
::deactivate_volumes
($storecfg, $vollist, $snapname) };
3920 # but only warn when that fails (e.g., parallel clones keeping them active)
3924 PVE
::Storage
::deactivate_volumes
($storecfg, $newvollist);
3926 my $newconffile = PVE
::QemuConfig-
>config_file($newid, $target);
3927 die "Failed to move config to node '$target' - rename failed: $!\n"
3928 if !rename($conffile, $newconffile);
3931 PVE
::AccessControl
::add_vm_to_pool
($newid, $pool) if $pool;
3934 eval { PVE
::QemuServer
::qemu_blockjobs_cancel
($vmid, $jobs) };
3935 sleep 1; # some storage like rbd need to wait before release volume - really?
3937 foreach my $volid (@$newvollist) {
3938 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
3942 PVE
::Firewall
::remove_vmfw_conf
($newid);
3944 unlink $conffile; # avoid races -> last thing before die
3946 die "clone failed: $err";
3952 # Aquire exclusive lock lock for $newid
3953 my $lock_target_vm = sub {
3954 return PVE
::QemuConfig-
>lock_config_full($newid, 1, $clonefn);
3957 my $lock_source_vm = sub {
3958 # exclusive lock if VM is running - else shared lock is enough;
3960 return PVE
::QemuConfig-
>lock_config_full($vmid, 1, $lock_target_vm);
3962 return PVE
::QemuConfig-
>lock_config_shared($vmid, 1, $lock_target_vm);
3966 $load_and_check->(); # early checks before forking/locking
3968 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $lock_source_vm);
3971 __PACKAGE__-
>register_method({
3972 name
=> 'move_vm_disk',
3973 path
=> '{vmid}/move_disk',
3977 description
=> "Move volume to different storage or to a different VM.",
3979 description
=> "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
3980 "and 'Datastore.AllocateSpace' permissions on the storage. To move ".
3981 "a disk to another VM, you need the permissions on the target VM as well.",
3982 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
3985 additionalProperties
=> 0,
3987 node
=> get_standard_option
('pve-node'),
3988 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3989 'target-vmid' => get_standard_option
('pve-vmid', {
3990 completion
=> \
&PVE
::QemuServer
::complete_vmid
,
3995 description
=> "The disk you want to move.",
3996 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
3998 storage
=> get_standard_option
('pve-storage-id', {
3999 description
=> "Target storage.",
4000 completion
=> \
&PVE
::QemuServer
::complete_storage
,
4005 description
=> "Target Format.",
4006 enum
=> [ 'raw', 'qcow2', 'vmdk' ],
4011 description
=> "Delete the original disk after successful copy. By default the"
4012 ." original disk is kept as unused disk.",
4018 description
=> 'Prevent changes if current configuration file has different SHA1"
4019 ." digest. This can be used to prevent concurrent modifications.',
4024 description
=> "Override I/O bandwidth limit (in KiB/s).",
4028 default => 'move limit from datacenter or storage config',
4032 description
=> "The config key the disk will be moved to on the target VM"
4033 ." (for example, ide0 or scsi1). Default is the source disk key.",
4034 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
4037 'target-digest' => {
4039 description
=> 'Prevent changes if the current config file of the target VM has a"
4040 ." different SHA1 digest. This can be used to detect concurrent modifications.',
4048 description
=> "the task ID.",
4053 my $rpcenv = PVE
::RPCEnvironment
::get
();
4054 my $authuser = $rpcenv->get_user();
4056 my $node = extract_param
($param, 'node');
4057 my $vmid = extract_param
($param, 'vmid');
4058 my $target_vmid = extract_param
($param, 'target-vmid');
4059 my $digest = extract_param
($param, 'digest');
4060 my $target_digest = extract_param
($param, 'target-digest');
4061 my $disk = extract_param
($param, 'disk');
4062 my $target_disk = extract_param
($param, 'target-disk') // $disk;
4063 my $storeid = extract_param
($param, 'storage');
4064 my $format = extract_param
($param, 'format');
4066 my $storecfg = PVE
::Storage
::config
();
4068 my $load_and_check_move = sub {
4069 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4070 PVE
::QemuConfig-
>check_lock($conf);
4072 PVE
::Tools
::assert_if_modified
($digest, $conf->{digest
});
4074 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4076 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
4078 die "disk '$disk' has no associated volume\n" if !$drive->{file
};
4079 die "you can't move a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
4081 my $old_volid = $drive->{file
};
4083 my ($oldstoreid, $oldvolname) = PVE
::Storage
::parse_volume_id
($old_volid);
4084 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
4088 die "you can't move to the same storage with same format\n"
4089 if $oldstoreid eq $storeid && (!$format || !$oldfmt || $oldfmt eq $format);
4091 # this only checks snapshots because $disk is passed!
4092 my $snapshotted = PVE
::QemuServer
::Drive
::is_volume_in_use
(
4098 die "you can't move a disk with snapshots and delete the source\n"
4099 if $snapshotted && $param->{delete};
4101 return ($conf, $drive, $oldstoreid, $snapshotted);
4104 my $move_updatefn = sub {
4105 my ($conf, $drive, $oldstoreid, $snapshotted) = $load_and_check_move->();
4106 my $old_volid = $drive->{file
};
4108 PVE
::Cluster
::log_msg
(
4111 "move disk VM $vmid: move --disk $disk --storage $storeid"
4114 my $running = PVE
::QemuServer
::check_running
($vmid);
4116 PVE
::Storage
::activate_volumes
($storecfg, [ $drive->{file
} ]);
4118 my $newvollist = [];
4124 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
4126 warn "moving disk with snapshots, snapshots will not be moved!\n"
4129 my $bwlimit = extract_param
($param, 'bwlimit');
4130 my $movelimit = PVE
::Storage
::get_bandwidth_limit
(
4132 [$oldstoreid, $storeid],
4138 running
=> $running,
4147 storage
=> $storeid,
4151 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($conf)
4152 if $disk eq 'efidisk0';
4154 my $newdrive = PVE
::QemuServer
::clone_disk
(
4165 $conf->{$disk} = PVE
::QemuServer
::print_drive
($newdrive);
4167 PVE
::QemuConfig-
>add_unused_volume($conf, $old_volid) if !$param->{delete};
4169 # convert moved disk to base if part of template
4170 PVE
::QemuServer
::template_create
($vmid, $conf, $disk)
4171 if PVE
::QemuConfig-
>is_template($conf);
4173 PVE
::QemuConfig-
>write_config($vmid, $conf);
4175 my $do_trim = PVE
::QemuServer
::get_qga_key
($conf, 'fstrim_cloned_disks');
4176 if ($running && $do_trim && PVE
::QemuServer
::qga_check_running
($vmid)) {
4177 eval { mon_cmd
($vmid, "guest-fstrim") };
4181 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
4182 PVE
::Storage
::deactivate_volumes
($storecfg, [ $newdrive->{file
} ])
4188 foreach my $volid (@$newvollist) {
4189 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid) };
4192 die "storage migration failed: $err";
4195 if ($param->{delete}) {
4197 PVE
::Storage
::deactivate_volumes
($storecfg, [$old_volid]);
4198 PVE
::Storage
::vdisk_free
($storecfg, $old_volid);
4204 my $load_and_check_reassign_configs = sub {
4205 my $vmlist = PVE
::Cluster
::get_vmlist
()->{ids
};
4207 die "could not find VM ${vmid}\n" if !exists($vmlist->{$vmid});
4208 die "could not find target VM ${target_vmid}\n" if !exists($vmlist->{$target_vmid});
4210 my $source_node = $vmlist->{$vmid}->{node
};
4211 my $target_node = $vmlist->{$target_vmid}->{node
};
4213 die "Both VMs need to be on the same node ($source_node != $target_node)\n"
4214 if $source_node ne $target_node;
4216 my $source_conf = PVE
::QemuConfig-
>load_config($vmid);
4217 PVE
::QemuConfig-
>check_lock($source_conf);
4218 my $target_conf = PVE
::QemuConfig-
>load_config($target_vmid);
4219 PVE
::QemuConfig-
>check_lock($target_conf);
4221 die "Can't move disks from or to template VMs\n"
4222 if ($source_conf->{template
} || $target_conf->{template
});
4225 eval { PVE
::Tools
::assert_if_modified
($digest, $source_conf->{digest
}) };
4226 die "VM ${vmid}: $@" if $@;
4229 if ($target_digest) {
4230 eval { PVE
::Tools
::assert_if_modified
($target_digest, $target_conf->{digest
}) };
4231 die "VM ${target_vmid}: $@" if $@;
4234 die "Disk '${disk}' for VM '$vmid' does not exist\n" if !defined($source_conf->{$disk});
4236 die "Target disk key '${target_disk}' is already in use for VM '$target_vmid'\n"
4237 if $target_conf->{$target_disk};
4239 my $drive = PVE
::QemuServer
::parse_drive
(
4241 $source_conf->{$disk},
4243 die "failed to parse source disk - $@\n" if !$drive;
4245 my $source_volid = $drive->{file
};
4247 die "disk '${disk}' has no associated volume\n" if !$source_volid;
4248 die "CD drive contents can't be moved to another VM\n"
4249 if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
4251 my $storeid = PVE
::Storage
::parse_volume_id
($source_volid, 1);
4252 die "Volume '$source_volid' not managed by PVE\n" if !defined($storeid);
4254 die "Can't move disk used by a snapshot to another VM\n"
4255 if PVE
::QemuServer
::Drive
::is_volume_in_use
($storecfg, $source_conf, $disk, $source_volid);
4256 die "Storage does not support moving of this disk to another VM\n"
4257 if (!PVE
::Storage
::volume_has_feature
($storecfg, 'rename', $source_volid));
4258 die "Cannot move disk to another VM while the source VM is running - detach first\n"
4259 if PVE
::QemuServer
::check_running
($vmid) && $disk !~ m/^unused\d+$/;
4261 # now re-parse using target disk slot format
4262 if ($target_disk =~ /^unused\d+$/) {
4263 $drive = PVE
::QemuServer
::parse_drive
(
4268 $drive = PVE
::QemuServer
::parse_drive
(
4270 $source_conf->{$disk},
4273 die "failed to parse source disk for target disk format - $@\n" if !$drive;
4275 my $repl_conf = PVE
::ReplicationConfig-
>new();
4276 if ($repl_conf->check_for_existing_jobs($target_vmid, 1)) {
4277 my $format = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4278 die "Cannot move disk to a replicated VM. Storage does not support replication!\n"
4279 if !PVE
::Storage
::storage_can_replicate
($storecfg, $storeid, $format);
4282 return ($source_conf, $target_conf, $drive);
4287 print STDERR
"$msg\n";
4290 my $disk_reassignfn = sub {
4291 return PVE
::QemuConfig-
>lock_config($vmid, sub {
4292 return PVE
::QemuConfig-
>lock_config($target_vmid, sub {
4293 my ($source_conf, $target_conf, $drive) = &$load_and_check_reassign_configs();
4295 my $source_volid = $drive->{file
};
4297 print "moving disk '$disk' from VM '$vmid' to '$target_vmid'\n";
4298 my ($storeid, $source_volname) = PVE
::Storage
::parse_volume_id
($source_volid);
4300 my $fmt = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4302 my $new_volid = PVE
::Storage
::rename_volume
(
4308 $drive->{file
} = $new_volid;
4310 my $boot_order = PVE
::QemuServer
::device_bootorder
($source_conf);
4311 if (defined(delete $boot_order->{$disk})) {
4312 print "removing disk '$disk' from boot order config\n";
4313 my $boot_devs = [ sort { $boot_order->{$a} <=> $boot_order->{$b} } keys %$boot_order ];
4314 $source_conf->{boot
} = PVE
::QemuServer
::print_bootorder
($boot_devs);
4317 delete $source_conf->{$disk};
4318 print "removing disk '${disk}' from VM '${vmid}' config\n";
4319 PVE
::QemuConfig-
>write_config($vmid, $source_conf);
4321 my $drive_string = PVE
::QemuServer
::print_drive
($drive);
4323 if ($target_disk =~ /^unused\d+$/) {
4324 $target_conf->{$target_disk} = $drive_string;
4325 PVE
::QemuConfig-
>write_config($target_vmid, $target_conf);
4330 vmid
=> $target_vmid,
4331 digest
=> $target_digest,
4332 $target_disk => $drive_string,
4338 # remove possible replication snapshots
4339 if (PVE
::Storage
::volume_has_feature
(
4345 PVE
::Replication
::prepare
(
4355 print "Failed to remove replication snapshots on moved disk " .
4356 "'$target_disk'. Manual cleanup could be necessary.\n";
4363 if ($target_vmid && $storeid) {
4364 my $msg = "either set 'storage' or 'target-vmid', but not both";
4365 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4366 } elsif ($target_vmid) {
4367 $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
4368 if $authuser ne 'root@pam';
4370 raise_param_exc
({ 'target-vmid' => "must be different than source VMID to reassign disk" })
4371 if $vmid eq $target_vmid;
4373 my (undef, undef, $drive) = &$load_and_check_reassign_configs();
4374 my $storage = PVE
::Storage
::parse_volume_id
($drive->{file
});
4375 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
4377 return $rpcenv->fork_worker(
4379 "${vmid}-${disk}>${target_vmid}-${target_disk}",
4383 } elsif ($storeid) {
4384 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4386 $load_and_check_move->(); # early checks before forking/locking
4389 PVE
::QemuConfig-
>lock_config($vmid, $move_updatefn);
4392 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
4394 my $msg = "both 'storage' and 'target-vmid' missing, either needs to be set";
4395 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4399 my $check_vm_disks_local = sub {
4400 my ($storecfg, $vmconf, $vmid) = @_;
4402 my $local_disks = {};
4404 # add some more information to the disks e.g. cdrom
4405 PVE
::QemuServer
::foreach_volid
($vmconf, sub {
4406 my ($volid, $attr) = @_;
4408 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
4410 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
4411 return if $scfg->{shared
};
4413 # The shared attr here is just a special case where the vdisk
4414 # is marked as shared manually
4415 return if $attr->{shared
};
4416 return if $attr->{cdrom
} and $volid eq "none";
4418 if (exists $local_disks->{$volid}) {
4419 @{$local_disks->{$volid}}{keys %$attr} = values %$attr
4421 $local_disks->{$volid} = $attr;
4422 # ensure volid is present in case it's needed
4423 $local_disks->{$volid}->{volid
} = $volid;
4427 return $local_disks;
4430 __PACKAGE__-
>register_method({
4431 name
=> 'migrate_vm_precondition',
4432 path
=> '{vmid}/migrate',
4436 description
=> "Get preconditions for migration.",
4438 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4441 additionalProperties
=> 0,
4443 node
=> get_standard_option
('pve-node'),
4444 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4445 target
=> get_standard_option
('pve-node', {
4446 description
=> "Target node.",
4447 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4455 running
=> { type
=> 'boolean' },
4459 description
=> "List nodes allowed for offline migration, only passed if VM is offline"
4461 not_allowed_nodes
=> {
4464 description
=> "List not allowed nodes with additional informations, only passed if VM is offline"
4468 description
=> "List local disks including CD-Rom, unsused and not referenced disks"
4470 local_resources
=> {
4472 description
=> "List local resources e.g. pci, usb"
4474 'mapped-resources' => {
4476 description
=> "List of mapped resources e.g. pci, usb"
4483 my $rpcenv = PVE
::RPCEnvironment
::get
();
4485 my $authuser = $rpcenv->get_user();
4487 PVE
::Cluster
::check_cfs_quorum
();
4491 my $vmid = extract_param
($param, 'vmid');
4492 my $target = extract_param
($param, 'target');
4493 my $localnode = PVE
::INotify
::nodename
();
4497 my $vmconf = PVE
::QemuConfig-
>load_config($vmid);
4498 my $storecfg = PVE
::Storage
::config
();
4501 # try to detect errors early
4502 PVE
::QemuConfig-
>check_lock($vmconf);
4504 $res->{running
} = PVE
::QemuServer
::check_running
($vmid) ?
1:0;
4506 my ($local_resources, $mapped_resources, $missing_mappings_by_node) =
4507 PVE
::QemuServer
::check_local_resources
($vmconf, 1);
4508 delete $missing_mappings_by_node->{$localnode};
4510 my $vga = PVE
::QemuServer
::parse_vga
($vmconf->{vga
});
4511 if ($res->{running
} && $vga->{'clipboard'} && $vga->{'clipboard'} eq 'vnc') {
4512 push $local_resources->@*, "clipboard=vnc";
4515 # if vm is not running, return target nodes where local storage/mapped devices are available
4516 # for offline migration
4517 if (!$res->{running
}) {
4518 $res->{allowed_nodes
} = [];
4519 my $checked_nodes = PVE
::QemuServer
::check_local_storage_availability
($vmconf, $storecfg);
4520 delete $checked_nodes->{$localnode};
4522 foreach my $node (keys %$checked_nodes) {
4523 my $missing_mappings = $missing_mappings_by_node->{$node};
4524 if (scalar($missing_mappings->@*)) {
4525 $checked_nodes->{$node}->{'unavailable-resources'} = $missing_mappings;
4529 if (!defined($checked_nodes->{$node}->{unavailable_storages
})) {
4530 push @{$res->{allowed_nodes
}}, $node;
4534 $res->{not_allowed_nodes
} = $checked_nodes;
4537 my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
4538 $res->{local_disks
} = [ values %$local_disks ];;
4540 $res->{local_resources
} = $local_resources;
4541 $res->{'mapped-resources'} = $mapped_resources;
4548 __PACKAGE__-
>register_method({
4549 name
=> 'migrate_vm',
4550 path
=> '{vmid}/migrate',
4554 description
=> "Migrate virtual machine. Creates a new migration task.",
4556 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4559 additionalProperties
=> 0,
4561 node
=> get_standard_option
('pve-node'),
4562 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4563 target
=> get_standard_option
('pve-node', {
4564 description
=> "Target node.",
4565 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4569 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4574 description
=> "Allow to migrate VMs which use local devices. Only root may use this option.",
4579 enum
=> ['secure', 'insecure'],
4580 description
=> "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
4583 migration_network
=> {
4584 type
=> 'string', format
=> 'CIDR',
4585 description
=> "CIDR of the (sub) network that is used for migration.",
4588 "with-local-disks" => {
4590 description
=> "Enable live storage migration for local disk",
4593 targetstorage
=> get_standard_option
('pve-targetstorage', {
4594 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4597 description
=> "Override I/O bandwidth limit (in KiB/s).",
4601 default => 'migrate limit from datacenter or storage config',
4607 description
=> "the task ID.",
4612 my $rpcenv = PVE
::RPCEnvironment
::get
();
4613 my $authuser = $rpcenv->get_user();
4615 my $target = extract_param
($param, 'target');
4617 my $localnode = PVE
::INotify
::nodename
();
4618 raise_param_exc
({ target
=> "target is local node."}) if $target eq $localnode;
4620 PVE
::Cluster
::check_cfs_quorum
();
4622 PVE
::Cluster
::check_node_exists
($target);
4624 my $targetip = PVE
::Cluster
::remote_node_ip
($target);
4626 my $vmid = extract_param
($param, 'vmid');
4628 raise_param_exc
({ force
=> "Only root may use this option." })
4629 if $param->{force
} && $authuser ne 'root@pam';
4631 raise_param_exc
({ migration_type
=> "Only root may use this option." })
4632 if $param->{migration_type
} && $authuser ne 'root@pam';
4634 # allow root only until better network permissions are available
4635 raise_param_exc
({ migration_network
=> "Only root may use this option." })
4636 if $param->{migration_network
} && $authuser ne 'root@pam';
4639 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4641 # try to detect errors early
4643 PVE
::QemuConfig-
>check_lock($conf);
4645 if (PVE
::QemuServer
::check_running
($vmid)) {
4646 die "can't migrate running VM without --online\n" if !$param->{online
};
4648 my $repl_conf = PVE
::ReplicationConfig-
>new();
4649 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
4650 my $is_replicated_to_target = defined($repl_conf->find_local_replication_job($vmid, $target));
4651 if (!$param->{force
} && $is_replicated && !$is_replicated_to_target) {
4652 die "Cannot live-migrate replicated VM to node '$target' - not a replication " .
4653 "target. Use 'force' to override.\n";
4656 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4657 $param->{online
} = 0;
4660 my $storecfg = PVE
::Storage
::config
();
4661 if (my $targetstorage = $param->{targetstorage
}) {
4662 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($targetstorage, 'pve-storage-id') };
4663 raise_param_exc
({ targetstorage
=> "failed to parse storage map: $@" })
4666 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk'])
4667 if !defined($storagemap->{identity
});
4669 foreach my $target_sid (values %{$storagemap->{entries
}}) {
4670 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $target_sid, $target);
4673 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storagemap->{default}, $target)
4674 if $storagemap->{default};
4676 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target)
4677 if $storagemap->{identity
};
4679 $param->{storagemap
} = $storagemap;
4681 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target);
4684 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
4689 print "Requesting HA migration for VM $vmid to node $target\n";
4691 my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
4692 PVE
::Tools
::run_command
($cmd);
4696 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
4701 PVE
::QemuMigrate-
>migrate($target, $targetip, $vmid, $param);
4705 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
4708 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
4713 __PACKAGE__-
>register_method({
4714 name
=> 'remote_migrate_vm',
4715 path
=> '{vmid}/remote_migrate',
4719 description
=> "Migrate virtual machine to a remote cluster. Creates a new migration task. EXPERIMENTAL feature!",
4721 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4724 additionalProperties
=> 0,
4726 node
=> get_standard_option
('pve-node'),
4727 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4728 'target-vmid' => get_standard_option
('pve-vmid', { optional
=> 1 }),
4729 'target-endpoint' => get_standard_option
('proxmox-remote', {
4730 description
=> "Remote target endpoint",
4734 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4739 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.",
4743 'target-storage' => get_standard_option
('pve-targetstorage', {
4744 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4747 'target-bridge' => {
4749 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.",
4750 format
=> 'bridge-pair-list',
4753 description
=> "Override I/O bandwidth limit (in KiB/s).",
4757 default => 'migrate limit from datacenter or storage config',
4763 description
=> "the task ID.",
4768 my $rpcenv = PVE
::RPCEnvironment
::get
();
4769 my $authuser = $rpcenv->get_user();
4771 my $source_vmid = extract_param
($param, 'vmid');
4772 my $target_endpoint = extract_param
($param, 'target-endpoint');
4773 my $target_vmid = extract_param
($param, 'target-vmid') // $source_vmid;
4775 my $delete = extract_param
($param, 'delete') // 0;
4777 PVE
::Cluster
::check_cfs_quorum
();
4780 my $conf = PVE
::QemuConfig-
>load_config($source_vmid);
4782 PVE
::QemuConfig-
>check_lock($conf);
4784 raise_param_exc
({ vmid
=> "cannot migrate HA-managed VM to remote cluster" })
4785 if PVE
::HA
::Config
::vm_is_ha_managed
($source_vmid);
4787 my $remote = PVE
::JSONSchema
::parse_property_string
('proxmox-remote', $target_endpoint);
4789 # TODO: move this as helper somewhere appropriate?
4791 protocol
=> 'https',
4792 host
=> $remote->{host
},
4793 port
=> $remote->{port
} // 8006,
4794 apitoken
=> $remote->{apitoken
},
4798 if ($fp = $remote->{fingerprint
}) {
4799 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 };
4802 print "Establishing API connection with remote at '$remote->{host}'\n";
4804 my $api_client = PVE
::APIClient
::LWP-
>new(%$conn_args);
4806 if (!defined($fp)) {
4807 my $cert_info = $api_client->get("/nodes/localhost/certificates/info");
4808 foreach my $cert (@$cert_info) {
4809 my $filename = $cert->{filename
};
4810 next if $filename ne 'pveproxy-ssl.pem' && $filename ne 'pve-ssl.pem';
4811 $fp = $cert->{fingerprint
} if !$fp || $filename eq 'pveproxy-ssl.pem';
4813 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 }
4817 my $repl_conf = PVE
::ReplicationConfig-
>new();
4818 my $is_replicated = $repl_conf->check_for_existing_jobs($source_vmid, 1);
4819 die "cannot remote-migrate replicated VM\n" if $is_replicated;
4821 if (PVE
::QemuServer
::check_running
($source_vmid)) {
4822 die "can't migrate running VM without --online\n" if !$param->{online
};
4825 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4826 $param->{online
} = 0;
4829 my $storecfg = PVE
::Storage
::config
();
4830 my $target_storage = extract_param
($param, 'target-storage');
4831 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($target_storage, 'pve-storage-id') };
4832 raise_param_exc
({ 'target-storage' => "failed to parse storage map: $@" })
4835 my $target_bridge = extract_param
($param, 'target-bridge');
4836 my $bridgemap = eval { PVE
::JSONSchema
::parse_idmap
($target_bridge, 'pve-bridge-id') };
4837 raise_param_exc
({ 'target-bridge' => "failed to parse bridge map: $@" })
4840 die "remote migration requires explicit storage mapping!\n"
4841 if $storagemap->{identity
};
4843 $param->{storagemap
} = $storagemap;
4844 $param->{bridgemap
} = $bridgemap;
4845 $param->{remote
} = {
4846 conn
=> $conn_args, # re-use fingerprint for tunnel
4847 client
=> $api_client,
4848 vmid
=> $target_vmid,
4850 $param->{migration_type
} = 'websocket';
4851 $param->{'with-local-disks'} = 1;
4852 $param->{delete} = $delete if $delete;
4854 my $cluster_status = $api_client->get("/cluster/status");
4856 foreach my $entry (@$cluster_status) {
4857 next if $entry->{type
} ne 'node';
4858 if ($entry->{local}) {
4859 $target_node = $entry->{name
};
4864 die "couldn't determine endpoint's node name\n"
4865 if !defined($target_node);
4868 PVE
::QemuMigrate-
>migrate($target_node, $remote->{host
}, $source_vmid, $param);
4872 return PVE
::GuestHelpers
::guest_migration_lock
($source_vmid, 10, $realcmd);
4875 return $rpcenv->fork_worker('qmigrate', $source_vmid, $authuser, $worker);
4878 __PACKAGE__-
>register_method({
4880 path
=> '{vmid}/monitor',
4884 description
=> "Execute QEMU monitor commands.",
4886 description
=> "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
4887 check
=> ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
4890 additionalProperties
=> 0,
4892 node
=> get_standard_option
('pve-node'),
4893 vmid
=> get_standard_option
('pve-vmid'),
4896 description
=> "The monitor command.",
4900 returns
=> { type
=> 'string'},
4904 my $rpcenv = PVE
::RPCEnvironment
::get
();
4905 my $authuser = $rpcenv->get_user();
4908 my $command = shift;
4909 return $command =~ m/^\s*info(\s+|$)/
4910 || $command =~ m/^\s*help\s*$/;
4913 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
4914 if !&$is_ro($param->{command
});
4916 my $vmid = $param->{vmid
};
4918 my $conf = PVE
::QemuConfig-
>load_config ($vmid); # check if VM exists
4922 $res = PVE
::QemuServer
::Monitor
::hmp_cmd
($vmid, $param->{command
});
4924 $res = "ERROR: $@" if $@;
4929 __PACKAGE__-
>register_method({
4930 name
=> 'resize_vm',
4931 path
=> '{vmid}/resize',
4935 description
=> "Extend volume size.",
4937 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
4940 additionalProperties
=> 0,
4942 node
=> get_standard_option
('pve-node'),
4943 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4944 skiplock
=> get_standard_option
('skiplock'),
4947 description
=> "The disk you want to resize.",
4948 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
4952 pattern
=> '\+?\d+(\.\d+)?[KMGT]?',
4953 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.",
4957 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
4965 description
=> "the task ID.",
4970 my $rpcenv = PVE
::RPCEnvironment
::get
();
4972 my $authuser = $rpcenv->get_user();
4974 my $node = extract_param
($param, 'node');
4976 my $vmid = extract_param
($param, 'vmid');
4978 my $digest = extract_param
($param, 'digest');
4980 my $disk = extract_param
($param, 'disk');
4982 my $sizestr = extract_param
($param, 'size');
4984 my $skiplock = extract_param
($param, 'skiplock');
4985 raise_param_exc
({ skiplock
=> "Only root may use this option." })
4986 if $skiplock && $authuser ne 'root@pam';
4988 my $storecfg = PVE
::Storage
::config
();
4990 my $updatefn = sub {
4992 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4994 die "checksum missmatch (file change by other user?)\n"
4995 if $digest && $digest ne $conf->{digest
};
4996 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
4998 die "disk '$disk' does not exist\n" if !$conf->{$disk};
5000 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
5002 my (undef, undef, undef, undef, undef, undef, $format) =
5003 PVE
::Storage
::parse_volname
($storecfg, $drive->{file
});
5005 my $volid = $drive->{file
};
5007 die "disk '$disk' has no associated volume\n" if !$volid;
5009 die "you can't resize a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive);
5011 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
5013 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
5015 PVE
::Storage
::activate_volumes
($storecfg, [$volid]);
5016 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid, 5);
5018 die "Could not determine current size of volume '$volid'\n" if !defined($size);
5020 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
5021 my ($ext, $newsize, $unit) = ($1, $2, $4);
5024 $newsize = $newsize * 1024;
5025 } elsif ($unit eq 'M') {
5026 $newsize = $newsize * 1024 * 1024;
5027 } elsif ($unit eq 'G') {
5028 $newsize = $newsize * 1024 * 1024 * 1024;
5029 } elsif ($unit eq 'T') {
5030 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
5033 $newsize += $size if $ext;
5034 $newsize = int($newsize);
5036 die "shrinking disks is not supported\n" if $newsize < $size;
5038 return if $size == $newsize;
5040 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
5042 PVE
::QemuServer
::qemu_block_resize
($vmid, "drive-$disk", $storecfg, $volid, $newsize);
5044 $drive->{size
} = $newsize;
5045 $conf->{$disk} = PVE
::QemuServer
::print_drive
($drive);
5047 PVE
::QemuConfig-
>write_config($vmid, $conf);
5051 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
5054 return $rpcenv->fork_worker('resize', $vmid, $authuser, $worker);
5057 __PACKAGE__-
>register_method({
5058 name
=> 'snapshot_list',
5059 path
=> '{vmid}/snapshot',
5061 description
=> "List all snapshots.",
5063 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5066 protected
=> 1, # qemu pid files are only readable by root
5068 additionalProperties
=> 0,
5070 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5071 node
=> get_standard_option
('pve-node'),
5080 description
=> "Snapshot identifier. Value 'current' identifies the current VM.",
5084 description
=> "Snapshot includes RAM.",
5089 description
=> "Snapshot description.",
5093 description
=> "Snapshot creation time",
5095 renderer
=> 'timestamp',
5099 description
=> "Parent snapshot identifier.",
5105 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
5110 my $vmid = $param->{vmid
};
5112 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5113 my $snaphash = $conf->{snapshots
} || {};
5117 foreach my $name (keys %$snaphash) {
5118 my $d = $snaphash->{$name};
5121 snaptime
=> $d->{snaptime
} || 0,
5122 vmstate
=> $d->{vmstate
} ?
1 : 0,
5123 description
=> $d->{description
} || '',
5125 $item->{parent
} = $d->{parent
} if $d->{parent
};
5126 $item->{snapstate
} = $d->{snapstate
} if $d->{snapstate
};
5130 my $running = PVE
::QemuServer
::check_running
($vmid, 1) ?
1 : 0;
5133 digest
=> $conf->{digest
},
5134 running
=> $running,
5135 description
=> "You are here!",
5137 $current->{parent
} = $conf->{parent
} if $conf->{parent
};
5139 push @$res, $current;
5144 __PACKAGE__-
>register_method({
5146 path
=> '{vmid}/snapshot',
5150 description
=> "Snapshot a VM.",
5152 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5155 additionalProperties
=> 0,
5157 node
=> get_standard_option
('pve-node'),
5158 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5159 snapname
=> get_standard_option
('pve-snapshot-name'),
5163 description
=> "Save the vmstate",
5168 description
=> "A textual description or comment.",
5174 description
=> "the task ID.",
5179 my $rpcenv = PVE
::RPCEnvironment
::get
();
5181 my $authuser = $rpcenv->get_user();
5183 my $node = extract_param
($param, 'node');
5185 my $vmid = extract_param
($param, 'vmid');
5187 my $snapname = extract_param
($param, 'snapname');
5189 die "unable to use snapshot name 'current' (reserved name)\n"
5190 if $snapname eq 'current';
5192 die "unable to use snapshot name 'pending' (reserved name)\n"
5193 if lc($snapname) eq 'pending';
5196 PVE
::Cluster
::log_msg
('info', $authuser, "snapshot VM $vmid: $snapname");
5197 PVE
::QemuConfig-
>snapshot_create($vmid, $snapname, $param->{vmstate
},
5198 $param->{description
});
5201 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
5204 __PACKAGE__-
>register_method({
5205 name
=> 'snapshot_cmd_idx',
5206 path
=> '{vmid}/snapshot/{snapname}',
5213 additionalProperties
=> 0,
5215 vmid
=> get_standard_option
('pve-vmid'),
5216 node
=> get_standard_option
('pve-node'),
5217 snapname
=> get_standard_option
('pve-snapshot-name'),
5226 links
=> [ { rel
=> 'child', href
=> "{cmd}" } ],
5233 push @$res, { cmd
=> 'rollback' };
5234 push @$res, { cmd
=> 'config' };
5239 __PACKAGE__-
>register_method({
5240 name
=> 'update_snapshot_config',
5241 path
=> '{vmid}/snapshot/{snapname}/config',
5245 description
=> "Update snapshot metadata.",
5247 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5250 additionalProperties
=> 0,
5252 node
=> get_standard_option
('pve-node'),
5253 vmid
=> get_standard_option
('pve-vmid'),
5254 snapname
=> get_standard_option
('pve-snapshot-name'),
5258 description
=> "A textual description or comment.",
5262 returns
=> { type
=> 'null' },
5266 my $rpcenv = PVE
::RPCEnvironment
::get
();
5268 my $authuser = $rpcenv->get_user();
5270 my $vmid = extract_param
($param, 'vmid');
5272 my $snapname = extract_param
($param, 'snapname');
5274 return if !defined($param->{description
});
5276 my $updatefn = sub {
5278 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5280 PVE
::QemuConfig-
>check_lock($conf);
5282 my $snap = $conf->{snapshots
}->{$snapname};
5284 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5286 $snap->{description
} = $param->{description
} if defined($param->{description
});
5288 PVE
::QemuConfig-
>write_config($vmid, $conf);
5291 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
5296 __PACKAGE__-
>register_method({
5297 name
=> 'get_snapshot_config',
5298 path
=> '{vmid}/snapshot/{snapname}/config',
5301 description
=> "Get snapshot configuration",
5303 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback', 'VM.Audit' ], any
=> 1],
5306 additionalProperties
=> 0,
5308 node
=> get_standard_option
('pve-node'),
5309 vmid
=> get_standard_option
('pve-vmid'),
5310 snapname
=> get_standard_option
('pve-snapshot-name'),
5313 returns
=> { type
=> "object" },
5317 my $rpcenv = PVE
::RPCEnvironment
::get
();
5319 my $authuser = $rpcenv->get_user();
5321 my $vmid = extract_param
($param, 'vmid');
5323 my $snapname = extract_param
($param, 'snapname');
5325 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5327 my $snap = $conf->{snapshots
}->{$snapname};
5329 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5334 __PACKAGE__-
>register_method({
5336 path
=> '{vmid}/snapshot/{snapname}/rollback',
5340 description
=> "Rollback VM state to specified snapshot.",
5342 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any
=> 1],
5345 additionalProperties
=> 0,
5347 node
=> get_standard_option
('pve-node'),
5348 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5349 snapname
=> get_standard_option
('pve-snapshot-name'),
5352 description
=> "Whether the VM should get started after rolling back successfully."
5353 . " (Note: VMs will be automatically started if the snapshot includes RAM.)",
5361 description
=> "the task ID.",
5366 my $rpcenv = PVE
::RPCEnvironment
::get
();
5368 my $authuser = $rpcenv->get_user();
5370 my $node = extract_param
($param, 'node');
5372 my $vmid = extract_param
($param, 'vmid');
5374 my $snapname = extract_param
($param, 'snapname');
5377 PVE
::Cluster
::log_msg
('info', $authuser, "rollback snapshot VM $vmid: $snapname");
5378 PVE
::QemuConfig-
>snapshot_rollback($vmid, $snapname);
5380 if ($param->{start
} && !PVE
::QemuServer
::Helpers
::vm_running_locally
($vmid)) {
5381 PVE
::API2
::Qemu-
>vm_start({ vmid
=> $vmid, node
=> $node });
5386 # hold migration lock, this makes sure that nobody create replication snapshots
5387 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
5390 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
5393 __PACKAGE__-
>register_method({
5394 name
=> 'delsnapshot',
5395 path
=> '{vmid}/snapshot/{snapname}',
5399 description
=> "Delete a VM snapshot.",
5401 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5404 additionalProperties
=> 0,
5406 node
=> get_standard_option
('pve-node'),
5407 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5408 snapname
=> get_standard_option
('pve-snapshot-name'),
5412 description
=> "For removal from config file, even if removing disk snapshots fails.",
5418 description
=> "the task ID.",
5423 my $rpcenv = PVE
::RPCEnvironment
::get
();
5425 my $authuser = $rpcenv->get_user();
5427 my $node = extract_param
($param, 'node');
5429 my $vmid = extract_param
($param, 'vmid');
5431 my $snapname = extract_param
($param, 'snapname');
5434 my $do_delete = sub {
5436 PVE
::Cluster
::log_msg
('info', $authuser, "delete snapshot VM $vmid: $snapname");
5437 PVE
::QemuConfig-
>snapshot_delete($vmid, $snapname, $param->{force
});
5441 if ($param->{force
}) {
5444 eval { PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $do_delete); };
5446 die $err if $lock_obtained;
5447 die "Failed to obtain guest migration lock - replication running?\n";
5452 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
5455 __PACKAGE__-
>register_method({
5457 path
=> '{vmid}/template',
5461 description
=> "Create a Template.",
5463 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid}",
5464 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
5467 additionalProperties
=> 0,
5469 node
=> get_standard_option
('pve-node'),
5470 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
5474 description
=> "If you want to convert only 1 disk to base image.",
5475 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
5482 description
=> "the task ID.",
5487 my $rpcenv = PVE
::RPCEnvironment
::get
();
5489 my $authuser = $rpcenv->get_user();
5491 my $node = extract_param
($param, 'node');
5493 my $vmid = extract_param
($param, 'vmid');
5495 my $disk = extract_param
($param, 'disk');
5497 my $load_and_check = sub {
5498 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5500 PVE
::QemuConfig-
>check_lock($conf);
5502 die "unable to create template, because VM contains snapshots\n"
5503 if $conf->{snapshots
} && scalar(keys %{$conf->{snapshots
}});
5505 die "you can't convert a template to a template\n"
5506 if PVE
::QemuConfig-
>is_template($conf) && !$disk;
5508 die "you can't convert a VM to template if VM is running\n"
5509 if PVE
::QemuServer
::check_running
($vmid);
5514 $load_and_check->();
5517 PVE
::QemuConfig-
>lock_config($vmid, sub {
5518 my $conf = $load_and_check->();
5520 $conf->{template
} = 1;
5521 PVE
::QemuConfig-
>write_config($vmid, $conf);
5523 PVE
::QemuServer
::template_create
($vmid, $conf, $disk);
5527 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
5530 __PACKAGE__-
>register_method({
5531 name
=> 'cloudinit_generated_config_dump',
5532 path
=> '{vmid}/cloudinit/dump',
5535 description
=> "Get automatically generated cloudinit config.",
5537 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5540 additionalProperties
=> 0,
5542 node
=> get_standard_option
('pve-node'),
5543 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5545 description
=> 'Config type.',
5547 enum
=> ['user', 'network', 'meta'],
5557 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
5559 return PVE
::QemuServer
::Cloudinit
::dump_cloudinit_config
($conf, $param->{vmid
}, $param->{type
});
5562 __PACKAGE__-
>register_method({
5564 path
=> '{vmid}/mtunnel',
5567 description
=> 'Migration tunnel endpoint - only for internal use by VM migration.',
5571 ['perm', '/vms/{vmid}', [ 'VM.Allocate' ]],
5572 ['perm', '/', [ 'Sys.Incoming' ]],
5574 description
=> "You need 'VM.Allocate' permissions on '/vms/{vmid}' and Sys.Incoming" .
5575 " on '/'. Further permission checks happen during the actual migration.",
5578 additionalProperties
=> 0,
5580 node
=> get_standard_option
('pve-node'),
5581 vmid
=> get_standard_option
('pve-vmid'),
5584 format
=> 'pve-storage-id-list',
5586 description
=> 'List of storages to check permission and availability. Will be checked again for all actually used storages during migration.',
5590 format
=> 'pve-bridge-id-list',
5592 description
=> 'List of network bridges to check availability. Will be checked again for actually used bridges during migration.',
5597 additionalProperties
=> 0,
5599 upid
=> { type
=> 'string' },
5600 ticket
=> { type
=> 'string' },
5601 socket => { type
=> 'string' },
5607 my $rpcenv = PVE
::RPCEnvironment
::get
();
5608 my $authuser = $rpcenv->get_user();
5610 my $node = extract_param
($param, 'node');
5611 my $vmid = extract_param
($param, 'vmid');
5613 my $storages = extract_param
($param, 'storages');
5614 my $bridges = extract_param
($param, 'bridges');
5616 my $nodename = PVE
::INotify
::nodename
();
5618 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
5619 if $node ne 'localhost' && $node ne $nodename;
5623 my $storecfg = PVE
::Storage
::config
();
5624 foreach my $storeid (PVE
::Tools
::split_list
($storages)) {
5625 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storeid, $node);
5628 foreach my $bridge (PVE
::Tools
::split_list
($bridges)) {
5629 PVE
::Network
::read_bridge_mtu
($bridge);
5632 PVE
::Cluster
::check_cfs_quorum
();
5634 my $lock = 'create';
5635 eval { PVE
::QemuConfig-
>create_and_lock_config($vmid, 0, $lock); };
5637 raise_param_exc
({ vmid
=> "unable to create empty VM config - $@"})
5642 storecfg
=> PVE
::Storage
::config
(),
5647 my $run_locked = sub {
5648 my ($code, $params) = @_;
5649 return PVE
::QemuConfig-
>lock_config($state->{vmid
}, sub {
5650 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5652 $state->{conf
} = $conf;
5654 die "Encountered wrong lock - aborting mtunnel command handling.\n"
5655 if $state->{lock} && !PVE
::QemuConfig-
>has_lock($conf, $state->{lock});
5657 return $code->($params);
5665 description
=> 'Full VM config, adapted for target cluster/node',
5667 'firewall-config' => {
5669 description
=> 'VM firewall config',
5674 format
=> PVE
::JSONSchema
::get_standard_option
('pve-qm-image-format'),
5677 format
=> 'pve-storage-id',
5681 description
=> 'parsed drive information without volid and format',
5687 description
=> 'params passed to vm_start_nolock',
5691 description
=> 'migrate_opts passed to vm_start_nolock',
5697 description
=> 'socket path for which the ticket should be valid. must be known to current mtunnel instance.',
5703 description
=> 'remove VM config and disks, aborting migration',
5707 'disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'disk-import'},
5708 'query-disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'query-disk-import'},
5709 bwlimit
=> $PVE::StorageTunnel
::cmd_schema-
>{bwlimit
},
5712 my $cmd_handlers = {
5714 # compared against other end's version
5715 # bump/reset for breaking changes
5716 # bump/bump for opt-in changes
5718 api
=> $PVE::QemuMigrate
::WS_TUNNEL_VERSION
,
5725 # parse and write out VM FW config if given
5726 if (my $fw_conf = $params->{'firewall-config'}) {
5727 my ($path, $fh) = PVE
::Tools
::tempfile_contents
($fw_conf, 700);
5734 ipset_comments
=> {},
5736 my $cluster_fw_conf = PVE
::Firewall
::load_clusterfw_conf
();
5738 # TODO: add flag for strict parsing?
5739 # TODO: add import sub that does all this given raw content?
5740 my $vmfw_conf = PVE
::Firewall
::generic_fw_config_parser
($path, $cluster_fw_conf, $empty_conf, 'vm');
5741 $vmfw_conf->{vmid
} = $state->{vmid
};
5742 PVE
::Firewall
::save_vmfw_conf
($state->{vmid
}, $vmfw_conf);
5744 $state->{cleanup
}->{fw
} = 1;
5747 my $conf_fn = "incoming/qemu-server/$state->{vmid}.conf";
5748 my $new_conf = PVE
::QemuServer
::parse_vm_config
($conf_fn, $params->{conf
}, 1);
5749 delete $new_conf->{lock};
5750 delete $new_conf->{digest
};
5752 # TODO handle properly?
5753 delete $new_conf->{snapshots
};
5754 delete $new_conf->{parent
};
5755 delete $new_conf->{pending
};
5757 # not handled by update_vm_api
5758 my $vmgenid = delete $new_conf->{vmgenid
};
5759 my $meta = delete $new_conf->{meta
};
5760 my $cloudinit = delete $new_conf->{cloudinit
}; # this is informational only
5761 $new_conf->{skip_cloud_init
} = 1; # re-use image from source side
5763 $new_conf->{vmid
} = $state->{vmid
};
5764 $new_conf->{node
} = $node;
5766 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, 'create');
5769 $update_vm_api->($new_conf, 1);
5772 # revert to locked previous config
5773 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5774 $conf->{lock} = 'create';
5775 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5780 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5781 $conf->{lock} = 'migrate';
5782 $conf->{vmgenid
} = $vmgenid if defined($vmgenid);
5783 $conf->{meta
} = $meta if defined($meta);
5784 $conf->{cloudinit
} = $cloudinit if defined($cloudinit);
5785 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5787 $state->{lock} = 'migrate';
5793 return PVE
::StorageTunnel
::handle_bwlimit
($params);
5798 my $format = $params->{format
};
5799 my $storeid = $params->{storage
};
5800 my $drive = $params->{drive
};
5802 $check_storage_access_migrate->($rpcenv, $authuser, $state->{storecfg
}, $storeid, $node);
5805 default => $storeid,
5808 my $source_volumes = {
5818 my $res = PVE
::QemuServer
::vm_migrate_alloc_nbd_disks
($state->{storecfg
}, $state->{vmid
}, $source_volumes, $storagemap);
5819 if (defined($res->{disk
})) {
5820 $state->{cleanup
}->{volumes
}->{$res->{disk
}->{volid
}} = 1;
5821 return $res->{disk
};
5823 die "failed to allocate NBD disk..\n";
5826 'disk-import' => sub {
5829 $check_storage_access_migrate->(
5837 $params->{unix
} = "/run/qemu-server/$state->{vmid}.storage";
5839 return PVE
::StorageTunnel
::handle_disk_import
($state, $params);
5841 'query-disk-import' => sub {
5844 return PVE
::StorageTunnel
::handle_query_disk_import
($state, $params);
5849 my $info = PVE
::QemuServer
::vm_start_nolock
(
5853 $params->{start_params
},
5854 $params->{migrate_opts
},
5858 if ($info->{migrate
}->{proto
} ne 'unix') {
5859 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5860 die "migration over non-UNIX sockets not possible\n";
5863 my $socket = $info->{migrate
}->{addr
};
5864 chown $state->{socket_uid
}, -1, $socket;
5865 $state->{sockets
}->{$socket} = 1;
5867 my $unix_sockets = $info->{migrate
}->{unix_sockets
};
5868 foreach my $socket (@$unix_sockets) {
5869 chown $state->{socket_uid
}, -1, $socket;
5870 $state->{sockets
}->{$socket} = 1;
5875 if (PVE
::QemuServer
::qga_check_running
($state->{vmid
})) {
5876 eval { mon_cmd
($state->{vmid
}, "guest-fstrim") };
5877 warn "fstrim failed: $@\n" if $@;
5882 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5886 PVE
::QemuServer
::nbd_stop
($state->{vmid
});
5890 if (PVE
::QemuServer
::Helpers
::vm_running_locally
($state->{vmid
})) {
5891 PVE
::QemuServer
::vm_resume
($state->{vmid
}, 1, 1);
5893 die "VM $state->{vmid} not running\n";
5898 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, $state->{lock});
5899 delete $state->{lock};
5905 my $path = $params->{path
};
5907 die "Not allowed to generate ticket for unknown socket '$path'\n"
5908 if !defined($state->{sockets
}->{$path});
5910 return { ticket
=> PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$path") };
5915 if ($params->{cleanup
}) {
5916 if ($state->{cleanup
}->{fw
}) {
5917 PVE
::Firewall
::remove_vmfw_conf
($state->{vmid
});
5920 for my $volid (keys $state->{cleanup
}->{volumes
}->%*) {
5921 print "freeing volume '$volid' as part of cleanup\n";
5922 eval { PVE
::Storage
::vdisk_free
($state->{storecfg
}, $volid) };
5926 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5929 print "switching to exit-mode, waiting for client to disconnect\n";
5936 my $socket_addr = "/run/qemu-server/$state->{vmid}.mtunnel";
5937 unlink $socket_addr;
5939 $state->{socket} = IO
::Socket
::UNIX-
>new(
5940 Type
=> SOCK_STREAM
(),
5941 Local
=> $socket_addr,
5945 $state->{socket_uid
} = getpwnam('www-data')
5946 or die "Failed to resolve user 'www-data' to numeric UID\n";
5947 chown $state->{socket_uid
}, -1, $socket_addr;
5950 print "mtunnel started\n";
5952 my $conn = eval { PVE
::Tools
::run_with_timeout
(300, sub { $state->{socket}->accept() }) };
5954 warn "Failed to accept tunnel connection - $@\n";
5956 warn "Removing tunnel socket..\n";
5957 unlink $state->{socket};
5959 warn "Removing temporary VM config..\n";
5961 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5964 die "Exiting mtunnel\n";
5967 $state->{conn
} = $conn;
5969 my $reply_err = sub {
5972 my $reply = JSON
::encode_json
({
5973 success
=> JSON
::false
,
5976 $conn->print("$reply\n");
5980 my $reply_ok = sub {
5983 $res->{success
} = JSON
::true
;
5984 my $reply = JSON
::encode_json
($res);
5985 $conn->print("$reply\n");
5989 while (my $line = <$conn>) {
5992 # untaint, we validate below if needed
5993 ($line) = $line =~ /^(.*)$/;
5994 my $parsed = eval { JSON
::decode_json
($line) };
5996 $reply_err->("failed to parse command - $@");
6000 my $cmd = delete $parsed->{cmd
};
6001 if (!defined($cmd)) {
6002 $reply_err->("'cmd' missing");
6003 } elsif ($state->{exit}) {
6004 $reply_err->("tunnel is in exit-mode, processing '$cmd' cmd not possible");
6006 } elsif (my $handler = $cmd_handlers->{$cmd}) {
6007 print "received command '$cmd'\n";
6009 if ($cmd_desc->{$cmd}) {
6010 PVE
::JSONSchema
::validate
($parsed, $cmd_desc->{$cmd});
6014 my $res = $run_locked->($handler, $parsed);
6017 $reply_err->("failed to handle '$cmd' command - $@")
6020 $reply_err->("unknown command '$cmd' given");
6024 if ($state->{exit}) {
6025 print "mtunnel exited\n";
6027 die "mtunnel exited unexpectedly\n";
6031 my $socket_addr = "/run/qemu-server/$vmid.mtunnel";
6032 my $ticket = PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$socket_addr");
6033 my $upid = $rpcenv->fork_worker('qmtunnel', $vmid, $authuser, $realcmd);
6038 socket => $socket_addr,
6042 __PACKAGE__-
>register_method({
6043 name
=> 'mtunnelwebsocket',
6044 path
=> '{vmid}/mtunnelwebsocket',
6047 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.",
6048 user
=> 'all', # check inside
6050 description
=> 'Migration tunnel endpoint for websocket upgrade - only for internal use by VM migration.',
6052 additionalProperties
=> 0,
6054 node
=> get_standard_option
('pve-node'),
6055 vmid
=> get_standard_option
('pve-vmid'),
6058 description
=> "unix socket to forward to",
6062 description
=> "ticket return by initial 'mtunnel' API call, or retrieved via 'ticket' tunnel command",
6069 port
=> { type
=> 'string', optional
=> 1 },
6070 socket => { type
=> 'string', optional
=> 1 },
6076 my $rpcenv = PVE
::RPCEnvironment
::get
();
6077 my $authuser = $rpcenv->get_user();
6079 my $nodename = PVE
::INotify
::nodename
();
6080 my $node = extract_param
($param, 'node');
6082 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
6083 if $node ne 'localhost' && $node ne $nodename;
6085 my $vmid = $param->{vmid
};
6087 PVE
::QemuConfig-
>load_config($vmid);
6089 my $socket = $param->{socket};
6090 PVE
::AccessControl
::verify_tunnel_ticket
($param->{ticket
}, $authuser, "/socket/$socket");
6092 return { socket => $socket };