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
::PCI
;
36 use PVE
::QemuServer
::USB
;
38 use PVE
::RPCEnvironment
;
39 use PVE
::AccessControl
;
43 use PVE
::API2
::Firewall
::VM
;
44 use PVE
::API2
::Qemu
::Agent
;
45 use PVE
::VZDump
::Plugin
;
46 use PVE
::DataCenterConfig
;
49 use PVE
::StorageTunnel
;
52 if (!$ENV{PVE_GENERATING_DOCS
}) {
53 require PVE
::HA
::Env
::PVE2
;
54 import PVE
::HA
::Env
::PVE2
;
55 require PVE
::HA
::Config
;
56 import PVE
::HA
::Config
;
60 use base
qw(PVE::RESTHandler);
62 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.";
64 my $resolve_cdrom_alias = sub {
67 if (my $value = $param->{cdrom
}) {
68 $value .= ",media=cdrom" if $value !~ m/media=/;
69 $param->{ide2
} = $value;
70 delete $param->{cdrom
};
74 # Used in import-enabled API endpoints. Parses drives using the extended '_with_alloc' schema.
75 my $foreach_volume_with_alloc = sub {
76 my ($param, $func) = @_;
78 for my $opt (sort keys $param->%*) {
79 next if !PVE
::QemuServer
::is_valid_drivename
($opt);
81 my $drive = PVE
::QemuServer
::Drive
::parse_drive
($opt, $param->{$opt}, 1);
84 $func->($opt, $drive);
88 my $NEW_DISK_RE = qr!^(([^/:\s]+):)?(\d+(\.\d+)?)$!;
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
} !~ $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 =~ $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 $create_disks = sub {
320 my ($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $settings, $default_storage) = @_;
327 my ($ds, $disk) = @_;
329 my $volid = $disk->{file
};
330 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
332 if (!$volid || $volid eq 'none' || $volid eq 'cdrom') {
333 delete $disk->{size
};
334 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
335 } elsif (defined($volname) && $volname eq 'cloudinit') {
336 $storeid = $storeid // $default_storage;
337 die "no storage ID specified (and no default storage)\n" if !$storeid;
340 my $ci_key = PVE
::QemuConfig-
>has_cloudinit($conf, $ds)
341 || PVE
::QemuConfig-
>has_cloudinit($conf->{pending
} || {}, $ds)
342 || PVE
::QemuConfig-
>has_cloudinit($res, $ds)
344 die "$ds - cloud-init drive is already attached at '$ci_key'\n";
347 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
348 my $name = "vm-$vmid-cloudinit";
352 $fmt = $disk->{format
} // "qcow2";
355 $fmt = $disk->{format
} // "raw";
358 # Initial disk created with 4 MB and aligned to 4MB on regeneration
359 my $ci_size = PVE
::QemuServer
::Cloudinit
::CLOUDINIT_DISK_SIZE
;
360 my $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid, $fmt, $name, $ci_size/1024);
361 $disk->{file
} = $volid;
362 $disk->{media
} = 'cdrom';
363 push @$vollist, $volid;
364 delete $disk->{format
}; # no longer needed
365 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
366 print "$ds: successfully created disk '$res->{$ds}'\n";
367 } elsif ($volid =~ $NEW_DISK_RE) {
368 my ($storeid, $size) = ($2 || $default_storage, $3);
369 die "no storage ID specified (and no default storage)\n" if !$storeid;
371 if (my $source = delete $disk->{'import-from'}) {
374 if (PVE
::Storage
::parse_volume_id
($source, 1)) { # PVE-managed volume
379 format
=> $disk->{format
},
382 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($conf, $disk)
383 if $ds eq 'efidisk0';
385 ($dst_volid, $size) = eval {
386 $import_from_volid->($storecfg, $source, $dest_info, $vollist);
388 die "cannot import from '$source' - $@" if $@;
390 $source = PVE
::Storage
::abs_filesystem_path
($storecfg, $source, 1);
391 $size = PVE
::Storage
::file_size_info
($source);
392 die "could not get file size of $source\n" if !$size;
394 (undef, $dst_volid) = PVE
::QemuServer
::ImportDisk
::do_import
(
400 format
=> $disk->{format
},
401 'skip-config-update' => 1,
404 push @$vollist, $dst_volid;
407 $disk->{file
} = $dst_volid;
408 $disk->{size
} = $size;
409 delete $disk->{format
}; # no longer needed
410 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
412 my $defformat = PVE
::Storage
::storage_default_format
($storecfg, $storeid);
413 my $fmt = $disk->{format
} || $defformat;
415 $size = PVE
::Tools
::convert_size
($size, 'gb' => 'kb'); # vdisk_alloc uses kb
418 if ($ds eq 'efidisk0') {
419 my $smm = PVE
::QemuServer
::Machine
::machine_type_is_q35
($conf);
420 ($volid, $size) = PVE
::QemuServer
::create_efidisk
(
421 $storecfg, $storeid, $vmid, $fmt, $arch, $disk, $smm);
422 } elsif ($ds eq 'tpmstate0') {
423 # swtpm can only use raw volumes, and uses a fixed size
424 $size = PVE
::Tools
::convert_size
(PVE
::QemuServer
::Drive
::TPMSTATE_DISK_SIZE
, 'b' => 'kb');
425 $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid, "raw", undef, $size);
427 $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid, $fmt, undef, $size);
429 push @$vollist, $volid;
430 $disk->{file
} = $volid;
431 $disk->{size
} = PVE
::Tools
::convert_size
($size, 'kb' => 'b');
432 delete $disk->{format
}; # no longer needed
433 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
436 print "$ds: successfully created disk '$res->{$ds}'\n";
438 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $volid);
440 my ($vtype) = PVE
::Storage
::parse_volname
($storecfg, $volid);
441 die "cannot use volume $volid - content type needs to be 'images' or 'iso'"
442 if $vtype ne 'images' && $vtype ne 'iso';
444 if (PVE
::QemuServer
::Drive
::drive_is_cloudinit
($disk)) {
446 my $ci_key = PVE
::QemuConfig-
>has_cloudinit($conf, $ds)
447 || PVE
::QemuConfig-
>has_cloudinit($conf->{pending
} || {}, $ds)
448 || PVE
::QemuConfig-
>has_cloudinit($res, $ds)
450 die "$ds - cloud-init drive is already attached at '$ci_key'\n";
455 PVE
::Storage
::activate_volumes
($storecfg, [ $volid ]) if $storeid;
457 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid);
458 die "volume $volid does not exist\n" if !$size;
459 $disk->{size
} = $size;
461 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
465 eval { $foreach_volume_with_alloc->($settings, $code); };
467 # free allocated images on error
469 syslog
('err', "VM $vmid creating disks failed");
470 foreach my $volid (@$vollist) {
471 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
477 return ($vollist, $res);
480 my $check_cpu_model_access = sub {
481 my ($rpcenv, $authuser, $new, $existing) = @_;
483 return if !defined($new->{cpu
});
485 my $cpu = PVE
::JSONSchema
::check_format
('pve-vm-cpu-conf', $new->{cpu
});
486 return if !$cpu || !$cpu->{cputype
}; # always allow default
487 my $cputype = $cpu->{cputype
};
489 if ($existing && $existing->{cpu
}) {
490 # changing only other settings doesn't require permissions for CPU model
491 my $existingCpu = PVE
::JSONSchema
::check_format
('pve-vm-cpu-conf', $existing->{cpu
});
492 return if $existingCpu->{cputype
} eq $cputype;
495 if (PVE
::QemuServer
::CPUConfig
::is_custom_model
($cputype)) {
496 $rpcenv->check($authuser, "/nodes", ['Sys.Audit']);
511 my $memoryoptions = {
517 my $hwtypeoptions = {
530 my $generaloptions = {
537 'migrate_downtime' => 1,
538 'migrate_speed' => 1,
550 my $vmpoweroptions = {
557 'vmstatestorage' => 1,
560 my $cloudinitoptions = {
570 my $check_vm_create_serial_perm = sub {
571 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
573 return 1 if $authuser eq 'root@pam';
575 foreach my $opt (keys %{$param}) {
576 next if $opt !~ m/^serial\d+$/;
578 if ($param->{$opt} eq 'socket') {
579 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
581 die "only root can set '$opt' config for real devices\n";
588 my sub check_usb_perm
{
589 my ($rpcenv, $authuser, $vmid, $pool, $opt, $value) = @_;
591 return 1 if $authuser eq 'root@pam';
593 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
595 my $device = PVE
::JSONSchema
::parse_property_string
('pve-qm-usb', $value);
596 if ($device->{host
} && $device->{host
} !~ m/^spice$/i) {
597 die "only root can set '$opt' config for real devices\n";
598 } elsif ($device->{mapping
}) {
599 $rpcenv->check_full($authuser, "/mapping/usb/$device->{mapping}", ['Mapping.Use']);
601 die "either 'host' or 'mapping' must be set.\n";
607 my sub check_vm_create_usb_perm
{
608 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
610 return 1 if $authuser eq 'root@pam';
612 foreach my $opt (keys %{$param}) {
613 next if $opt !~ m/^usb\d+$/;
614 check_usb_perm
($rpcenv, $authuser, $vmid, $pool, $opt, $param->{$opt});
620 my sub check_hostpci_perm
{
621 my ($rpcenv, $authuser, $vmid, $pool, $opt, $value) = @_;
623 return 1 if $authuser eq 'root@pam';
625 my $device = PVE
::JSONSchema
::parse_property_string
('pve-qm-hostpci', $value);
626 if ($device->{host
}) {
627 die "only root can set '$opt' config for non-mapped devices\n";
628 } elsif ($device->{mapping
}) {
629 $rpcenv->check_full($authuser, "/mapping/pci/$device->{mapping}", ['Mapping.Use']);
630 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
632 die "either 'host' or 'mapping' must be set.\n";
638 my sub check_vm_create_hostpci_perm
{
639 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
641 return 1 if $authuser eq 'root@pam';
643 foreach my $opt (keys %{$param}) {
644 next if $opt !~ m/^hostpci\d+$/;
645 check_hostpci_perm
($rpcenv, $authuser, $vmid, $pool, $opt, $param->{$opt});
651 my $check_vm_modify_config_perm = sub {
652 my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
654 return 1 if $authuser eq 'root@pam';
656 foreach my $opt (@$key_list) {
657 # some checks (e.g., disk, serial port, usb) need to be done somewhere
658 # else, as there the permission can be value dependend
659 next if PVE
::QemuServer
::is_valid_drivename
($opt);
660 next if $opt eq 'cdrom';
661 next if $opt =~ m/^(?:unused|serial|usb|hostpci)\d+$/;
662 next if $opt eq 'tags';
665 if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
666 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']);
667 } elsif ($memoryoptions->{$opt}) {
668 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']);
669 } elsif ($hwtypeoptions->{$opt}) {
670 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
671 } elsif ($generaloptions->{$opt}) {
672 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
673 # special case for startup since it changes host behaviour
674 if ($opt eq 'startup') {
675 $rpcenv->check_full($authuser, "/", ['Sys.Modify']);
677 } elsif ($vmpoweroptions->{$opt}) {
678 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']);
679 } elsif ($diskoptions->{$opt}) {
680 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
681 } elsif ($opt =~ m/^net\d+$/) {
682 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
683 } elsif ($cloudinitoptions->{$opt} || $opt =~ m/^ipconfig\d+$/) {
684 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Cloudinit', 'VM.Config.Network'], 1);
685 } elsif ($opt eq 'vmstate') {
686 # the user needs Disk and PowerMgmt privileges to change the vmstate
687 # also needs privileges on the storage, that will be checked later
688 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk', 'VM.PowerMgmt' ]);
690 # catches args, lock, etc.
691 # new options will be checked here
692 die "only root can set '$opt' config\n";
699 __PACKAGE__-
>register_method({
703 description
=> "Virtual machine index (per node).",
705 description
=> "Only list VMs where you have VM.Audit permissons on /vms/<vmid>.",
709 protected
=> 1, # qemu pid files are only readable by root
711 additionalProperties
=> 0,
713 node
=> get_standard_option
('pve-node'),
717 description
=> "Determine the full status of active VMs.",
725 properties
=> $PVE::QemuServer
::vmstatus_return_properties
,
727 links
=> [ { rel
=> 'child', href
=> "{vmid}" } ],
732 my $rpcenv = PVE
::RPCEnvironment
::get
();
733 my $authuser = $rpcenv->get_user();
735 my $vmstatus = PVE
::QemuServer
::vmstatus
(undef, $param->{full
});
738 foreach my $vmid (keys %$vmstatus) {
739 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
741 my $data = $vmstatus->{$vmid};
748 my $parse_restore_archive = sub {
749 my ($storecfg, $archive) = @_;
751 my ($archive_storeid, $archive_volname) = PVE
::Storage
::parse_volume_id
($archive, 1);
755 if (defined($archive_storeid)) {
756 my $scfg = PVE
::Storage
::storage_config
($storecfg, $archive_storeid);
757 $res->{volid
} = $archive;
758 if ($scfg->{type
} eq 'pbs') {
759 $res->{type
} = 'pbs';
763 my $path = PVE
::Storage
::abs_filesystem_path
($storecfg, $archive);
764 $res->{type
} = 'file';
765 $res->{path
} = $path;
770 __PACKAGE__-
>register_method({
774 description
=> "Create or restore a virtual machine.",
776 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
777 "For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
778 "If you create disks you need 'Datastore.AllocateSpace' on any used storage." .
779 "If you use a bridge/vlan, you need 'SDN.Use' on any used bridge/vlan.",
780 user
=> 'all', # check inside
785 additionalProperties
=> 0,
786 properties
=> PVE
::QemuServer
::json_config_properties
(
788 node
=> get_standard_option
('pve-node'),
789 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::Cluster
::complete_next_vmid
}),
791 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.",
795 completion
=> \
&PVE
::QemuServer
::complete_backup_archives
,
797 storage
=> get_standard_option
('pve-storage-id', {
798 description
=> "Default storage.",
800 completion
=> \
&PVE
::QemuServer
::complete_storage
,
805 description
=> "Allow to overwrite existing VM.",
806 requires
=> 'archive',
811 description
=> "Assign a unique random ethernet address.",
812 requires
=> 'archive',
817 description
=> "Start the VM immediately from the backup and restore in background. PBS only.",
818 requires
=> 'archive',
822 type
=> 'string', format
=> 'pve-poolid',
823 description
=> "Add the VM to the specified pool.",
826 description
=> "Override I/O bandwidth limit (in KiB/s).",
830 default => 'restore limit from datacenter or storage config',
836 description
=> "Start VM after it was created successfully.",
848 my $rpcenv = PVE
::RPCEnvironment
::get
();
849 my $authuser = $rpcenv->get_user();
851 my $node = extract_param
($param, 'node');
852 my $vmid = extract_param
($param, 'vmid');
854 my $archive = extract_param
($param, 'archive');
855 my $is_restore = !!$archive;
857 my $bwlimit = extract_param
($param, 'bwlimit');
858 my $force = extract_param
($param, 'force');
859 my $pool = extract_param
($param, 'pool');
860 my $start_after_create = extract_param
($param, 'start');
861 my $storage = extract_param
($param, 'storage');
862 my $unique = extract_param
($param, 'unique');
863 my $live_restore = extract_param
($param, 'live-restore');
865 if (defined(my $ssh_keys = $param->{sshkeys
})) {
866 $ssh_keys = URI
::Escape
::uri_unescape
($ssh_keys);
867 PVE
::Tools
::validate_ssh_public_keys
($ssh_keys);
870 $param->{cpuunits
} = PVE
::CGroup
::clamp_cpu_shares
($param->{cpuunits
})
871 if defined($param->{cpuunits
}); # clamp value depending on cgroup version
873 PVE
::Cluster
::check_cfs_quorum
();
875 my $filename = PVE
::QemuConfig-
>config_file($vmid);
876 my $storecfg = PVE
::Storage
::config
();
878 if (defined($pool)) {
879 $rpcenv->check_pool_exist($pool);
882 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace'])
883 if defined($storage);
885 if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
887 } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
889 } elsif ($archive && $force && (-f
$filename) &&
890 $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
891 # OK: user has VM.Backup permissions and wants to restore an existing VM
897 for my $opt (sort keys $param->%*) {
898 if (PVE
::QemuServer
::Drive
::is_valid_drivename
($opt)) {
899 raise_param_exc
({ $opt => "option conflicts with option 'archive'" });
903 if ($archive eq '-') {
904 die "pipe requires cli environment\n" if $rpcenv->{type
} ne 'cli';
905 $archive = { type
=> 'pipe' };
907 PVE
::Storage
::check_volume_access
(
916 $archive = $parse_restore_archive->($storecfg, $archive);
920 if (scalar(keys $param->%*) > 0) {
921 &$resolve_cdrom_alias($param);
923 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param, $storage);
925 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
927 &$check_vm_create_serial_perm($rpcenv, $authuser, $vmid, $pool, $param);
928 check_vm_create_usb_perm
($rpcenv, $authuser, $vmid, $pool, $param);
929 check_vm_create_hostpci_perm
($rpcenv, $authuser, $vmid, $pool, $param);
931 PVE
::QemuServer
::check_bridge_access
($rpcenv, $authuser, $param);
932 &$check_cpu_model_access($rpcenv, $authuser, $param);
934 $check_drive_param->($param, $storecfg);
936 PVE
::QemuServer
::add_random_macs
($param);
939 my $emsg = $is_restore ?
"unable to restore VM $vmid -" : "unable to create VM $vmid -";
941 eval { PVE
::QemuConfig-
>create_and_lock_config($vmid, $force) };
942 die "$emsg $@" if $@;
944 my $restored_data = 0;
945 my $restorefn = sub {
946 my $conf = PVE
::QemuConfig-
>load_config($vmid);
948 PVE
::QemuConfig-
>check_protection($conf, $emsg);
950 die "$emsg vm is running\n" if PVE
::QemuServer
::check_running
($vmid);
953 my $restore_options = {
958 live
=> $live_restore,
959 override_conf
=> $param,
961 if (my $volid = $archive->{volid
}) {
962 # best effort, real check is after restoring!
964 my $old_conf = PVE
::Storage
::extract_vzdump_config
($storecfg, $volid);
965 PVE
::QemuServer
::restore_merge_config
("backup/qemu-server/$vmid.conf", $old_conf, $param);
968 warn "Could not extract backed up config: $@\n";
969 warn "Skipping early checks!\n";
971 PVE
::QemuServer
::check_restore_permissions
($rpcenv, $authuser, $merged);
974 if ($archive->{type
} eq 'file' || $archive->{type
} eq 'pipe') {
975 die "live-restore is only compatible with backup images from a Proxmox Backup Server\n"
977 PVE
::QemuServer
::restore_file_archive
($archive->{path
} // '-', $vmid, $authuser, $restore_options);
978 } elsif ($archive->{type
} eq 'pbs') {
979 PVE
::QemuServer
::restore_proxmox_backup_archive
($archive->{volid
}, $vmid, $authuser, $restore_options);
981 die "unknown backup archive type\n";
985 my $restored_conf = PVE
::QemuConfig-
>load_config($vmid);
986 # Convert restored VM to template if backup was VM template
987 if (PVE
::QemuConfig-
>is_template($restored_conf)) {
988 warn "Convert to template.\n";
989 eval { PVE
::QemuServer
::template_create
($vmid, $restored_conf) };
994 # ensure no old replication state are exists
995 PVE
::ReplicationState
::delete_guest_states
($vmid);
997 PVE
::QemuConfig-
>lock_config_full($vmid, 1, $realcmd);
999 if ($start_after_create && !$live_restore) {
1000 print "Execute autostart\n";
1001 eval { PVE
::API2
::Qemu-
>vm_start({ vmid
=> $vmid, node
=> $node }) };
1006 my $createfn = sub {
1007 # ensure no old replication state are exists
1008 PVE
::ReplicationState
::delete_guest_states
($vmid);
1012 my $arch = PVE
::QemuServer
::get_vm_arch
($conf);
1014 $conf->{meta
} = PVE
::QemuServer
::new_meta_info_string
();
1018 ($vollist, my $created_opts) = $create_disks->(
1029 $conf->{$_} = $created_opts->{$_} for keys $created_opts->%*;
1031 if (!$conf->{boot
}) {
1032 my $devs = PVE
::QemuServer
::get_default_bootdevices
($conf);
1033 $conf->{boot
} = PVE
::QemuServer
::print_bootorder
($devs);
1036 # auto generate uuid if user did not specify smbios1 option
1037 if (!$conf->{smbios1
}) {
1038 $conf->{smbios1
} = PVE
::QemuServer
::generate_smbios1_uuid
();
1041 if ((!defined($conf->{vmgenid
}) || $conf->{vmgenid
} eq '1') && $arch ne 'aarch64') {
1042 $conf->{vmgenid
} = PVE
::QemuServer
::generate_uuid
();
1045 my $machine = $conf->{machine
};
1046 if (!$machine || $machine =~ m/^(?:pc|q35|virt)$/) {
1047 # always pin Windows' machine version on create, they get to easily confused
1048 if (PVE
::QemuServer
::Helpers
::windows_version
($conf->{ostype
})) {
1049 $conf->{machine
} = PVE
::QemuServer
::windows_get_pinned_machine_version
($machine);
1053 PVE
::QemuConfig-
>write_config($vmid, $conf);
1059 foreach my $volid (@$vollist) {
1060 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
1066 PVE
::AccessControl
::add_vm_to_pool
($vmid, $pool) if $pool;
1069 PVE
::QemuConfig-
>lock_config_full($vmid, 1, $realcmd);
1071 if ($start_after_create) {
1072 print "Execute autostart\n";
1073 eval { PVE
::API2
::Qemu-
>vm_start({vmid
=> $vmid, node
=> $node}) };
1078 my ($code, $worker_name);
1080 $worker_name = 'qmrestore';
1082 eval { $restorefn->() };
1084 eval { PVE
::QemuConfig-
>remove_lock($vmid, 'create') };
1086 if ($restored_data) {
1087 warn "error after data was restored, VM disks should be OK but config may "
1088 ."require adaptions. VM $vmid state is NOT cleaned up.\n";
1090 warn "error before or during data restore, some or all disks were not "
1091 ."completely restored. VM $vmid state is NOT cleaned up.\n";
1097 $worker_name = 'qmcreate';
1099 eval { $createfn->() };
1102 my $conffile = PVE
::QemuConfig-
>config_file($vmid);
1103 unlink($conffile) or die "failed to remove config file: $!\n";
1111 return $rpcenv->fork_worker($worker_name, $vmid, $authuser, $code);
1114 __PACKAGE__-
>register_method({
1119 description
=> "Directory index",
1124 additionalProperties
=> 0,
1126 node
=> get_standard_option
('pve-node'),
1127 vmid
=> get_standard_option
('pve-vmid'),
1135 subdir
=> { type
=> 'string' },
1138 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
1144 { subdir
=> 'config' },
1145 { subdir
=> 'cloudinit' },
1146 { subdir
=> 'pending' },
1147 { subdir
=> 'status' },
1148 { subdir
=> 'unlink' },
1149 { subdir
=> 'vncproxy' },
1150 { subdir
=> 'termproxy' },
1151 { subdir
=> 'migrate' },
1152 { subdir
=> 'resize' },
1153 { subdir
=> 'move' },
1154 { subdir
=> 'rrd' },
1155 { subdir
=> 'rrddata' },
1156 { subdir
=> 'monitor' },
1157 { subdir
=> 'agent' },
1158 { subdir
=> 'snapshot' },
1159 { subdir
=> 'spiceproxy' },
1160 { subdir
=> 'sendkey' },
1161 { subdir
=> 'firewall' },
1162 { subdir
=> 'mtunnel' },
1163 { subdir
=> 'remote_migrate' },
1169 __PACKAGE__-
>register_method ({
1170 subclass
=> "PVE::API2::Firewall::VM",
1171 path
=> '{vmid}/firewall',
1174 __PACKAGE__-
>register_method ({
1175 subclass
=> "PVE::API2::Qemu::Agent",
1176 path
=> '{vmid}/agent',
1179 __PACKAGE__-
>register_method({
1181 path
=> '{vmid}/rrd',
1183 protected
=> 1, # fixme: can we avoid that?
1185 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1187 description
=> "Read VM RRD statistics (returns PNG)",
1189 additionalProperties
=> 0,
1191 node
=> get_standard_option
('pve-node'),
1192 vmid
=> get_standard_option
('pve-vmid'),
1194 description
=> "Specify the time frame you are interested in.",
1196 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
1199 description
=> "The list of datasources you want to display.",
1200 type
=> 'string', format
=> 'pve-configid-list',
1203 description
=> "The RRD consolidation function",
1205 enum
=> [ 'AVERAGE', 'MAX' ],
1213 filename
=> { type
=> 'string' },
1219 return PVE
::RRD
::create_rrd_graph
(
1220 "pve2-vm/$param->{vmid}", $param->{timeframe
},
1221 $param->{ds
}, $param->{cf
});
1225 __PACKAGE__-
>register_method({
1227 path
=> '{vmid}/rrddata',
1229 protected
=> 1, # fixme: can we avoid that?
1231 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1233 description
=> "Read VM RRD statistics",
1235 additionalProperties
=> 0,
1237 node
=> get_standard_option
('pve-node'),
1238 vmid
=> get_standard_option
('pve-vmid'),
1240 description
=> "Specify the time frame you are interested in.",
1242 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
1245 description
=> "The RRD consolidation function",
1247 enum
=> [ 'AVERAGE', 'MAX' ],
1262 return PVE
::RRD
::create_rrd_data
(
1263 "pve2-vm/$param->{vmid}", $param->{timeframe
}, $param->{cf
});
1267 __PACKAGE__-
>register_method({
1268 name
=> 'vm_config',
1269 path
=> '{vmid}/config',
1272 description
=> "Get the virtual machine configuration with pending configuration " .
1273 "changes applied. Set the 'current' parameter to get the current configuration instead.",
1275 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1278 additionalProperties
=> 0,
1280 node
=> get_standard_option
('pve-node'),
1281 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1283 description
=> "Get current values (instead of pending values).",
1288 snapshot
=> get_standard_option
('pve-snapshot-name', {
1289 description
=> "Fetch config values from given snapshot.",
1292 my ($cmd, $pname, $cur, $args) = @_;
1293 PVE
::QemuConfig-
>snapshot_list($args->[0]);
1299 description
=> "The VM configuration.",
1301 properties
=> PVE
::QemuServer
::json_config_properties
({
1304 description
=> 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
1311 raise_param_exc
({ snapshot
=> "cannot use 'snapshot' parameter with 'current'",
1312 current
=> "cannot use 'snapshot' parameter with 'current'"})
1313 if ($param->{snapshot
} && $param->{current
});
1316 if ($param->{snapshot
}) {
1317 $conf = PVE
::QemuConfig-
>load_snapshot_config($param->{vmid
}, $param->{snapshot
});
1319 $conf = PVE
::QemuConfig-
>load_current_config($param->{vmid
}, $param->{current
});
1321 $conf->{cipassword
} = '**********' if $conf->{cipassword
};
1326 __PACKAGE__-
>register_method({
1327 name
=> 'vm_pending',
1328 path
=> '{vmid}/pending',
1331 description
=> "Get the virtual machine configuration with both current and pending values.",
1333 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1336 additionalProperties
=> 0,
1338 node
=> get_standard_option
('pve-node'),
1339 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1348 description
=> "Configuration option name.",
1352 description
=> "Current value.",
1357 description
=> "Pending value.",
1362 description
=> "Indicates a pending delete request if present and not 0. " .
1363 "The value 2 indicates a force-delete request.",
1375 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
1377 my $pending_delete_hash = PVE
::QemuConfig-
>parse_pending_delete($conf->{pending
}->{delete});
1379 $conf->{cipassword
} = '**********' if defined($conf->{cipassword
});
1380 $conf->{pending
}->{cipassword
} = '********** ' if defined($conf->{pending
}->{cipassword
});
1382 return PVE
::GuestHelpers
::config_with_pending_array
($conf, $pending_delete_hash);
1385 __PACKAGE__-
>register_method({
1386 name
=> 'cloudinit_pending',
1387 path
=> '{vmid}/cloudinit',
1390 description
=> "Get the cloudinit configuration with both current and pending values.",
1392 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1395 additionalProperties
=> 0,
1397 node
=> get_standard_option
('pve-node'),
1398 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1407 description
=> "Configuration option name.",
1411 description
=> "Value as it was used to generate the current cloudinit image.",
1416 description
=> "The new pending value.",
1421 description
=> "Indicates a pending delete request if present and not 0. ",
1433 my $vmid = $param->{vmid
};
1434 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1436 my $ci = $conf->{cloudinit
};
1438 $conf->{cipassword
} = '**********' if exists $conf->{cipassword
};
1439 $ci->{cipassword
} = '**********' if exists $ci->{cipassword
};
1443 # All the values that got added
1444 my $added = delete($ci->{added
}) // '';
1445 for my $key (PVE
::Tools
::split_list
($added)) {
1446 push @$res, { key
=> $key, pending
=> $conf->{$key} };
1449 # All already existing values (+ their new value, if it exists)
1450 for my $opt (keys %$cloudinitoptions) {
1451 next if !$conf->{$opt};
1452 next if $added =~ m/$opt/;
1457 if (my $pending = $ci->{$opt}) {
1458 $item->{value
} = $pending;
1459 $item->{pending
} = $conf->{$opt};
1461 $item->{value
} = $conf->{$opt},
1467 # Now, we'll find the deleted ones
1468 for my $opt (keys %$ci) {
1469 next if $conf->{$opt};
1470 push @$res, { key
=> $opt, delete => 1 };
1476 __PACKAGE__-
>register_method({
1477 name
=> 'cloudinit_update',
1478 path
=> '{vmid}/cloudinit',
1482 description
=> "Regenerate and change cloudinit config drive.",
1484 check
=> ['perm', '/vms/{vmid}', 'VM.Config.Cloudinit'],
1487 additionalProperties
=> 0,
1489 node
=> get_standard_option
('pve-node'),
1490 vmid
=> get_standard_option
('pve-vmid'),
1493 returns
=> { type
=> 'null' },
1497 my $rpcenv = PVE
::RPCEnvironment
::get
();
1498 my $authuser = $rpcenv->get_user();
1500 my $vmid = extract_param
($param, 'vmid');
1502 PVE
::QemuConfig-
>lock_config($vmid, sub {
1503 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1504 PVE
::QemuConfig-
>check_lock($conf);
1506 my $storecfg = PVE
::Storage
::config
();
1507 PVE
::QemuServer
::vmconfig_update_cloudinit_drive
($storecfg, $conf, $vmid);
1512 # POST/PUT {vmid}/config implementation
1514 # The original API used PUT (idempotent) an we assumed that all operations
1515 # are fast. But it turned out that almost any configuration change can
1516 # involve hot-plug actions, or disk alloc/free. Such actions can take long
1517 # time to complete and have side effects (not idempotent).
1519 # The new implementation uses POST and forks a worker process. We added
1520 # a new option 'background_delay'. If specified we wait up to
1521 # 'background_delay' second for the worker task to complete. It returns null
1522 # if the task is finished within that time, else we return the UPID.
1524 my $update_vm_api = sub {
1525 my ($param, $sync) = @_;
1527 my $rpcenv = PVE
::RPCEnvironment
::get
();
1529 my $authuser = $rpcenv->get_user();
1531 my $node = extract_param
($param, 'node');
1533 my $vmid = extract_param
($param, 'vmid');
1535 my $digest = extract_param
($param, 'digest');
1537 my $background_delay = extract_param
($param, 'background_delay');
1539 my $skip_cloud_init = extract_param
($param, 'skip_cloud_init');
1541 if (defined(my $cipassword = $param->{cipassword
})) {
1542 # Same logic as in cloud-init (but with the regex fixed...)
1543 $param->{cipassword
} = PVE
::Tools
::encrypt_pw
($cipassword)
1544 if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
1547 my @paramarr = (); # used for log message
1548 foreach my $key (sort keys %$param) {
1549 my $value = $key eq 'cipassword' ?
'<hidden>' : $param->{$key};
1550 push @paramarr, "-$key", $value;
1553 my $skiplock = extract_param
($param, 'skiplock');
1554 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1555 if $skiplock && $authuser ne 'root@pam';
1557 my $delete_str = extract_param
($param, 'delete');
1559 my $revert_str = extract_param
($param, 'revert');
1561 my $force = extract_param
($param, 'force');
1563 if (defined(my $ssh_keys = $param->{sshkeys
})) {
1564 $ssh_keys = URI
::Escape
::uri_unescape
($ssh_keys);
1565 PVE
::Tools
::validate_ssh_public_keys
($ssh_keys);
1568 $param->{cpuunits
} = PVE
::CGroup
::clamp_cpu_shares
($param->{cpuunits
})
1569 if defined($param->{cpuunits
}); # clamp value depending on cgroup version
1571 die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
1573 my $storecfg = PVE
::Storage
::config
();
1575 my $defaults = PVE
::QemuServer
::load_defaults
();
1577 &$resolve_cdrom_alias($param);
1579 # now try to verify all parameters
1582 foreach my $opt (PVE
::Tools
::split_list
($revert_str)) {
1583 if (!PVE
::QemuServer
::option_exists
($opt)) {
1584 raise_param_exc
({ revert
=> "unknown option '$opt'" });
1587 raise_param_exc
({ delete => "you can't use '-$opt' and " .
1588 "-revert $opt' at the same time" })
1589 if defined($param->{$opt});
1591 $revert->{$opt} = 1;
1595 foreach my $opt (PVE
::Tools
::split_list
($delete_str)) {
1596 $opt = 'ide2' if $opt eq 'cdrom';
1598 raise_param_exc
({ delete => "you can't use '-$opt' and " .
1599 "-delete $opt' at the same time" })
1600 if defined($param->{$opt});
1602 raise_param_exc
({ revert
=> "you can't use '-delete $opt' and " .
1603 "-revert $opt' at the same time" })
1606 if (!PVE
::QemuServer
::option_exists
($opt)) {
1607 raise_param_exc
({ delete => "unknown option '$opt'" });
1613 my $repl_conf = PVE
::ReplicationConfig-
>new();
1614 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
1615 my $check_replication = sub {
1617 return if !$is_replicated;
1618 my $volid = $drive->{file
};
1619 return if !$volid || !($drive->{replicate
}//1);
1620 return if PVE
::QemuServer
::drive_is_cdrom
($drive);
1622 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
1623 die "cannot add non-managed/pass-through volume to a replicated VM\n"
1624 if !defined($storeid);
1626 return if defined($volname) && $volname eq 'cloudinit';
1629 if ($volid =~ $NEW_DISK_RE) {
1631 $format = $drive->{format
} || PVE
::Storage
::storage_default_format
($storecfg, $storeid);
1633 $format = (PVE
::Storage
::parse_volname
($storecfg, $volid))[6];
1635 return if PVE
::Storage
::storage_can_replicate
($storecfg, $storeid, $format);
1636 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
1637 return if $scfg->{shared
};
1638 die "cannot add non-replicatable volume to a replicated VM\n";
1641 $check_drive_param->($param, $storecfg, $check_replication);
1643 foreach my $opt (keys %$param) {
1644 if ($opt =~ m/^net(\d+)$/) {
1646 my $net = PVE
::QemuServer
::parse_net
($param->{$opt});
1647 $param->{$opt} = PVE
::QemuServer
::print_net
($net);
1648 } elsif ($opt eq 'vmgenid') {
1649 if ($param->{$opt} eq '1') {
1650 $param->{$opt} = PVE
::QemuServer
::generate_uuid
();
1652 } elsif ($opt eq 'hookscript') {
1653 eval { PVE
::GuestHelpers
::check_hookscript
($param->{$opt}, $storecfg); };
1654 raise_param_exc
({ $opt => $@ }) if $@;
1658 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]);
1660 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
1662 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
1664 PVE
::QemuServer
::check_bridge_access
($rpcenv, $authuser, $param);
1666 my $updatefn = sub {
1668 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1670 die "checksum missmatch (file change by other user?)\n"
1671 if $digest && $digest ne $conf->{digest
};
1673 &$check_cpu_model_access($rpcenv, $authuser, $param, $conf);
1675 # FIXME: 'suspended' lock should probabyl be a state or "weak" lock?!
1676 if (scalar(@delete) && grep { $_ eq 'vmstate'} @delete) {
1677 if (defined($conf->{lock}) && $conf->{lock} eq 'suspended') {
1678 delete $conf->{lock}; # for check lock check, not written out
1679 push @delete, 'lock'; # this is the real deal to write it out
1681 push @delete, 'runningmachine' if $conf->{runningmachine
};
1682 push @delete, 'runningcpu' if $conf->{runningcpu
};
1685 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
1687 foreach my $opt (keys %$revert) {
1688 if (defined($conf->{$opt})) {
1689 $param->{$opt} = $conf->{$opt};
1690 } elsif (defined($conf->{pending
}->{$opt})) {
1695 if ($param->{memory
} || defined($param->{balloon
})) {
1696 my $maxmem = $param->{memory
} || $conf->{pending
}->{memory
} || $conf->{memory
} || $defaults->{memory
};
1697 my $balloon = defined($param->{balloon
}) ?
$param->{balloon
} : $conf->{pending
}->{balloon
} || $conf->{balloon
};
1699 die "balloon value too large (must be smaller than assigned memory)\n"
1700 if $balloon && $balloon > $maxmem;
1703 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: " . join (' ', @paramarr));
1707 print "update VM $vmid: " . join (' ', @paramarr) . "\n";
1709 # write updates to pending section
1711 my $modified = {}; # record what $option we modify
1714 if (my $boot = $conf->{boot
}) {
1715 my $bootcfg = PVE
::JSONSchema
::parse_property_string
('pve-qm-boot', $boot);
1716 @bootorder = PVE
::Tools
::split_list
($bootcfg->{order
}) if $bootcfg && $bootcfg->{order
};
1718 my $bootorder_deleted = grep {$_ eq 'bootorder'} @delete;
1720 my $check_drive_perms = sub {
1721 my ($opt, $val) = @_;
1722 my $drive = PVE
::QemuServer
::parse_drive
($opt, $val, 1);
1723 if (PVE
::QemuServer
::drive_is_cloudinit
($drive)) {
1724 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Cloudinit', 'VM.Config.CDROM']);
1725 } elsif (PVE
::QemuServer
::drive_is_cdrom
($drive, 1)) { # CDROM
1726 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1728 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1733 foreach my $opt (@delete) {
1734 $modified->{$opt} = 1;
1735 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1737 # value of what we want to delete, independent if pending or not
1738 my $val = $conf->{$opt} // $conf->{pending
}->{$opt};
1739 if (!defined($val)) {
1740 warn "cannot delete '$opt' - not set in current configuration!\n";
1741 $modified->{$opt} = 0;
1744 my $is_pending_val = defined($conf->{pending
}->{$opt});
1745 delete $conf->{pending
}->{$opt};
1747 # remove from bootorder if necessary
1748 if (!$bootorder_deleted && @bootorder && grep {$_ eq $opt} @bootorder) {
1749 @bootorder = grep {$_ ne $opt} @bootorder;
1750 $conf->{pending
}->{boot
} = PVE
::QemuServer
::print_bootorder
(\
@bootorder);
1751 $modified->{boot
} = 1;
1754 if ($opt =~ m/^unused/) {
1755 my $drive = PVE
::QemuServer
::parse_drive
($opt, $val);
1756 PVE
::QemuConfig-
>check_protection($conf, "can't remove unused disk '$drive->{file}'");
1757 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1758 if (PVE
::QemuServer
::try_deallocate_drive
($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1759 delete $conf->{$opt};
1760 PVE
::QemuConfig-
>write_config($vmid, $conf);
1762 } elsif ($opt eq 'vmstate') {
1763 PVE
::QemuConfig-
>check_protection($conf, "can't remove vmstate '$val'");
1764 if (PVE
::QemuServer
::try_deallocate_drive
($storecfg, $vmid, $conf, $opt, { file
=> $val }, $rpcenv, $authuser, 1)) {
1765 delete $conf->{$opt};
1766 PVE
::QemuConfig-
>write_config($vmid, $conf);
1768 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
1769 PVE
::QemuConfig-
>check_protection($conf, "can't remove drive '$opt'");
1770 $check_drive_perms->($opt, $val);
1771 PVE
::QemuServer
::vmconfig_register_unused_drive
($storecfg, $vmid, $conf, PVE
::QemuServer
::parse_drive
($opt, $val))
1773 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1774 PVE
::QemuConfig-
>write_config($vmid, $conf);
1775 } elsif ($opt =~ m/^serial\d+$/) {
1776 if ($val eq 'socket') {
1777 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1778 } elsif ($authuser ne 'root@pam') {
1779 die "only root can delete '$opt' config for real devices\n";
1781 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1782 PVE
::QemuConfig-
>write_config($vmid, $conf);
1783 } elsif ($opt =~ m/^usb\d+$/) {
1784 check_usb_perm
($rpcenv, $authuser, $vmid, undef, $opt, $val);
1785 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1786 PVE
::QemuConfig-
>write_config($vmid, $conf);
1787 } elsif ($opt =~ m/^hostpci\d+$/) {
1788 check_hostpci_perm
($rpcenv, $authuser, $vmid, undef, $opt, $val);
1789 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1790 PVE
::QemuConfig-
>write_config($vmid, $conf);
1791 } elsif ($opt eq 'tags') {
1792 assert_tag_permissions
($vmid, $val, '', $rpcenv, $authuser);
1793 delete $conf->{$opt};
1794 PVE
::QemuConfig-
>write_config($vmid, $conf);
1796 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1797 PVE
::QemuConfig-
>write_config($vmid, $conf);
1801 foreach my $opt (keys %$param) { # add/change
1802 $modified->{$opt} = 1;
1803 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1804 next if defined($conf->{pending
}->{$opt}) && ($param->{$opt} eq $conf->{pending
}->{$opt}); # skip if nothing changed
1806 my $arch = PVE
::QemuServer
::get_vm_arch
($conf);
1808 if (PVE
::QemuServer
::is_valid_drivename
($opt)) {
1810 if ($conf->{$opt}) {
1811 $check_drive_perms->($opt, $conf->{$opt});
1815 $check_drive_perms->($opt, $param->{$opt});
1816 PVE
::QemuServer
::vmconfig_register_unused_drive
($storecfg, $vmid, $conf, PVE
::QemuServer
::parse_drive
($opt, $conf->{pending
}->{$opt}))
1817 if defined($conf->{pending
}->{$opt});
1819 my (undef, $created_opts) = $create_disks->(
1827 {$opt => $param->{$opt}},
1829 $conf->{pending
}->{$_} = $created_opts->{$_} for keys $created_opts->%*;
1831 # default legacy boot order implies all cdroms anyway
1833 # append new CD drives to bootorder to mark them bootable
1834 my $drive = PVE
::QemuServer
::parse_drive
($opt, $param->{$opt}, 1);
1835 if (PVE
::QemuServer
::drive_is_cdrom
($drive, 1) && !grep(/^$opt$/, @bootorder)) {
1836 push @bootorder, $opt;
1837 $conf->{pending
}->{boot
} = PVE
::QemuServer
::print_bootorder
(\
@bootorder);
1838 $modified->{boot
} = 1;
1841 } elsif ($opt =~ m/^serial\d+/) {
1842 if ((!defined($conf->{$opt}) || $conf->{$opt} eq 'socket') && $param->{$opt} eq 'socket') {
1843 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1844 } elsif ($authuser ne 'root@pam') {
1845 die "only root can modify '$opt' config for real devices\n";
1847 $conf->{pending
}->{$opt} = $param->{$opt};
1848 } elsif ($opt =~ m/^usb\d+/) {
1849 if (my $olddevice = $conf->{$opt}) {
1850 check_usb_perm
($rpcenv, $authuser, $vmid, undef, $opt, $conf->{$opt});
1852 check_usb_perm
($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
1853 $conf->{pending
}->{$opt} = $param->{$opt};
1854 } elsif ($opt =~ m/^hostpci\d+$/) {
1855 if (my $oldvalue = $conf->{$opt}) {
1856 check_hostpci_perm
($rpcenv, $authuser, $vmid, undef, $opt, $oldvalue);
1858 check_hostpci_perm
($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
1859 $conf->{pending
}->{$opt} = $param->{$opt};
1860 } elsif ($opt eq 'tags') {
1861 assert_tag_permissions
($vmid, $conf->{$opt}, $param->{$opt}, $rpcenv, $authuser);
1862 $conf->{pending
}->{$opt} = PVE
::GuestHelpers
::get_unique_tags
($param->{$opt});
1864 $conf->{pending
}->{$opt} = $param->{$opt};
1866 if ($opt eq 'boot') {
1867 my $new_bootcfg = PVE
::JSONSchema
::parse_property_string
('pve-qm-boot', $param->{$opt});
1868 if ($new_bootcfg->{order
}) {
1869 my @devs = PVE
::Tools
::split_list
($new_bootcfg->{order
});
1870 for my $dev (@devs) {
1871 my $exists = $conf->{$dev} || $conf->{pending
}->{$dev} || $param->{$dev};
1872 my $deleted = grep {$_ eq $dev} @delete;
1873 die "invalid bootorder: device '$dev' does not exist'\n"
1874 if !$exists || $deleted;
1877 # remove legacy boot order settings if new one set
1878 $conf->{pending
}->{$opt} = PVE
::QemuServer
::print_bootorder
(\
@devs);
1879 PVE
::QemuConfig-
>add_to_pending_delete($conf, "bootdisk")
1880 if $conf->{bootdisk
};
1884 PVE
::QemuConfig-
>remove_from_pending_delete($conf, $opt);
1885 PVE
::QemuConfig-
>write_config($vmid, $conf);
1888 # remove pending changes when nothing changed
1889 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1890 my $changes = PVE
::QemuConfig-
>cleanup_pending($conf);
1891 PVE
::QemuConfig-
>write_config($vmid, $conf) if $changes;
1893 return if !scalar(keys %{$conf->{pending
}});
1895 my $running = PVE
::QemuServer
::check_running
($vmid);
1897 # apply pending changes
1899 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1903 PVE
::QemuServer
::vmconfig_hotplug_pending
($vmid, $conf, $storecfg, $modified, $errors);
1905 # cloud_init must be skipped if we are in an incoming, remote live migration
1906 PVE
::QemuServer
::vmconfig_apply_pending
($vmid, $conf, $storecfg, $errors, $skip_cloud_init);
1908 raise_param_exc
($errors) if scalar(keys %$errors);
1917 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
1919 if ($background_delay) {
1921 # Note: It would be better to do that in the Event based HTTPServer
1922 # to avoid blocking call to sleep.
1924 my $end_time = time() + $background_delay;
1926 my $task = PVE
::Tools
::upid_decode
($upid);
1929 while (time() < $end_time) {
1930 $running = PVE
::ProcFSTools
::check_process_running
($task->{pid
}, $task->{pstart
});
1932 sleep(1); # this gets interrupted when child process ends
1936 my $status = PVE
::Tools
::upid_read_status
($upid);
1937 return if !PVE
::Tools
::upid_status_is_error
($status);
1938 die "failed to update VM $vmid: $status\n";
1946 return PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
1949 my $vm_config_perm_list = [
1954 'VM.Config.Network',
1956 'VM.Config.Options',
1957 'VM.Config.Cloudinit',
1960 __PACKAGE__-
>register_method({
1961 name
=> 'update_vm_async',
1962 path
=> '{vmid}/config',
1966 description
=> "Set virtual machine options (asynchrounous API).",
1968 check
=> ['perm', '/vms/{vmid}', $vm_config_perm_list, any
=> 1],
1971 additionalProperties
=> 0,
1972 properties
=> PVE
::QemuServer
::json_config_properties
(
1974 node
=> get_standard_option
('pve-node'),
1975 vmid
=> get_standard_option
('pve-vmid'),
1976 skiplock
=> get_standard_option
('skiplock'),
1978 type
=> 'string', format
=> 'pve-configid-list',
1979 description
=> "A list of settings you want to delete.",
1983 type
=> 'string', format
=> 'pve-configid-list',
1984 description
=> "Revert a pending change.",
1989 description
=> $opt_force_description,
1991 requires
=> 'delete',
1995 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1999 background_delay
=> {
2001 description
=> "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
2007 1, # with_disk_alloc
2014 code
=> $update_vm_api,
2017 __PACKAGE__-
>register_method({
2018 name
=> 'update_vm',
2019 path
=> '{vmid}/config',
2023 description
=> "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
2025 check
=> ['perm', '/vms/{vmid}', $vm_config_perm_list, any
=> 1],
2028 additionalProperties
=> 0,
2029 properties
=> PVE
::QemuServer
::json_config_properties
(
2031 node
=> get_standard_option
('pve-node'),
2032 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2033 skiplock
=> get_standard_option
('skiplock'),
2035 type
=> 'string', format
=> 'pve-configid-list',
2036 description
=> "A list of settings you want to delete.",
2040 type
=> 'string', format
=> 'pve-configid-list',
2041 description
=> "Revert a pending change.",
2046 description
=> $opt_force_description,
2048 requires
=> 'delete',
2052 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2057 1, # with_disk_alloc
2060 returns
=> { type
=> 'null' },
2063 &$update_vm_api($param, 1);
2068 __PACKAGE__-
>register_method({
2069 name
=> 'destroy_vm',
2074 description
=> "Destroy the VM and all used/owned volumes. Removes any VM specific permissions"
2075 ." and firewall rules",
2077 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
2080 additionalProperties
=> 0,
2082 node
=> get_standard_option
('pve-node'),
2083 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
2084 skiplock
=> get_standard_option
('skiplock'),
2087 description
=> "Remove VMID from configurations, like backup & replication jobs and HA.",
2090 'destroy-unreferenced-disks' => {
2092 description
=> "If set, destroy additionally all disks not referenced in the config"
2093 ." but with a matching VMID from all enabled storages.",
2105 my $rpcenv = PVE
::RPCEnvironment
::get
();
2106 my $authuser = $rpcenv->get_user();
2107 my $vmid = $param->{vmid
};
2109 my $skiplock = $param->{skiplock
};
2110 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2111 if $skiplock && $authuser ne 'root@pam';
2113 my $early_checks = sub {
2115 my $conf = PVE
::QemuConfig-
>load_config($vmid);
2116 PVE
::QemuConfig-
>check_protection($conf, "can't remove VM $vmid");
2118 my $ha_managed = PVE
::HA
::Config
::service_is_configured
("vm:$vmid");
2120 if (!$param->{purge
}) {
2121 die "unable to remove VM $vmid - used in HA resources and purge parameter not set.\n"
2123 # don't allow destroy if with replication jobs but no purge param
2124 my $repl_conf = PVE
::ReplicationConfig-
>new();
2125 $repl_conf->check_for_existing_jobs($vmid);
2128 die "VM $vmid is running - destroy failed\n"
2129 if PVE
::QemuServer
::check_running
($vmid);
2139 my $storecfg = PVE
::Storage
::config
();
2141 syslog
('info', "destroy VM $vmid: $upid\n");
2142 PVE
::QemuConfig-
>lock_config($vmid, sub {
2143 # repeat, config might have changed
2144 my $ha_managed = $early_checks->();
2146 my $purge_unreferenced = $param->{'destroy-unreferenced-disks'};
2148 PVE
::QemuServer
::destroy_vm
(
2151 $skiplock, { lock => 'destroyed' },
2152 $purge_unreferenced,
2155 PVE
::AccessControl
::remove_vm_access
($vmid);
2156 PVE
::Firewall
::remove_vmfw_conf
($vmid);
2157 if ($param->{purge
}) {
2158 print "purging VM $vmid from related configurations..\n";
2159 PVE
::ReplicationConfig
::remove_vmid_jobs
($vmid);
2160 PVE
::VZDump
::Plugin
::remove_vmid_from_backup_jobs
($vmid);
2163 PVE
::HA
::Config
::delete_service_from_config
("vm:$vmid");
2164 print "NOTE: removed VM $vmid from HA resource configuration.\n";
2168 # only now remove the zombie config, else we can have reuse race
2169 PVE
::QemuConfig-
>destroy_config($vmid);
2173 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
2176 __PACKAGE__-
>register_method({
2178 path
=> '{vmid}/unlink',
2182 description
=> "Unlink/delete disk images.",
2184 check
=> [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
2187 additionalProperties
=> 0,
2189 node
=> get_standard_option
('pve-node'),
2190 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2192 type
=> 'string', format
=> 'pve-configid-list',
2193 description
=> "A list of disk IDs you want to delete.",
2197 description
=> $opt_force_description,
2202 returns
=> { type
=> 'null'},
2206 $param->{delete} = extract_param
($param, 'idlist');
2208 __PACKAGE__-
>update_vm($param);
2213 # uses good entropy, each char is limited to 6 bit to get printable chars simply
2214 my $gen_rand_chars = sub {
2217 die "invalid length $length" if $length < 1;
2219 my $min = ord('!'); # first printable ascii
2221 my $rand_bytes = Crypt
::OpenSSL
::Random
::random_bytes
($length);
2222 die "failed to generate random bytes!\n"
2225 my $str = join('', map { chr((ord($_) & 0x3F) + $min) } split('', $rand_bytes));
2232 __PACKAGE__-
>register_method({
2234 path
=> '{vmid}/vncproxy',
2238 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2240 description
=> "Creates a TCP VNC proxy connections.",
2242 additionalProperties
=> 0,
2244 node
=> get_standard_option
('pve-node'),
2245 vmid
=> get_standard_option
('pve-vmid'),
2249 description
=> "starts websockify instead of vncproxy",
2251 'generate-password' => {
2255 description
=> "Generates a random password to be used as ticket instead of the API ticket.",
2260 additionalProperties
=> 0,
2262 user
=> { type
=> 'string' },
2263 ticket
=> { type
=> 'string' },
2266 description
=> "Returned if requested with 'generate-password' param."
2267 ." Consists of printable ASCII characters ('!' .. '~').",
2270 cert
=> { type
=> 'string' },
2271 port
=> { type
=> 'integer' },
2272 upid
=> { type
=> 'string' },
2278 my $rpcenv = PVE
::RPCEnvironment
::get
();
2280 my $authuser = $rpcenv->get_user();
2282 my $vmid = $param->{vmid
};
2283 my $node = $param->{node
};
2284 my $websocket = $param->{websocket
};
2286 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # check if VM exists
2290 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2291 $serial = $vga->{type
} if $vga->{type
} =~ m/^serial\d+$/;
2294 my $authpath = "/vms/$vmid";
2296 my $ticket = PVE
::AccessControl
::assemble_vnc_ticket
($authuser, $authpath);
2297 my $password = $ticket;
2298 if ($param->{'generate-password'}) {
2299 $password = $gen_rand_chars->(8);
2302 $sslcert = PVE
::Tools
::file_get_contents
("/etc/pve/pve-root-ca.pem", 8192)
2308 if ($node ne 'localhost' && $node ne PVE
::INotify
::nodename
()) {
2309 (undef, $family) = PVE
::Cluster
::remote_node_ip
($node);
2310 my $sshinfo = PVE
::SSHInfo
::get_ssh_info
($node);
2311 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
2312 $remcmd = PVE
::SSHInfo
::ssh_info_to_command
($sshinfo, defined($serial) ?
'-t' : '-T');
2314 $family = PVE
::Tools
::get_host_address_family
($node);
2317 my $port = PVE
::Tools
::next_vnc_port
($family);
2324 syslog
('info', "starting vnc proxy $upid\n");
2328 if (defined($serial)) {
2330 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $serial, '-escape', '0' ];
2332 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
2333 '-timeout', $timeout, '-authpath', $authpath,
2334 '-perm', 'Sys.Console'];
2336 if ($param->{websocket
}) {
2337 $ENV{PVE_VNC_TICKET
} = $password; # pass ticket to vncterm
2338 push @$cmd, '-notls', '-listen', 'localhost';
2341 push @$cmd, '-c', @$remcmd, @$termcmd;
2343 PVE
::Tools
::run_command
($cmd);
2347 $ENV{LC_PVE_TICKET
} = $password if $websocket; # set ticket with "qm vncproxy"
2349 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
2351 my $sock = IO
::Socket
::IP-
>new(
2356 GetAddrInfoFlags
=> 0,
2357 ) or die "failed to create socket: $!\n";
2358 # Inside the worker we shouldn't have any previous alarms
2359 # running anyway...:
2361 local $SIG{ALRM
} = sub { die "connection timed out\n" };
2363 accept(my $cli, $sock) or die "connection failed: $!\n";
2366 if (PVE
::Tools
::run_command
($cmd,
2367 output
=> '>&'.fileno($cli),
2368 input
=> '<&'.fileno($cli),
2371 die "Failed to run vncproxy.\n";
2378 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
2380 PVE
::Tools
::wait_for_vnc_port
($port);
2389 $res->{password
} = $password if $param->{'generate-password'};
2394 __PACKAGE__-
>register_method({
2395 name
=> 'termproxy',
2396 path
=> '{vmid}/termproxy',
2400 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2402 description
=> "Creates a TCP proxy connections.",
2404 additionalProperties
=> 0,
2406 node
=> get_standard_option
('pve-node'),
2407 vmid
=> get_standard_option
('pve-vmid'),
2411 enum
=> [qw(serial0 serial1 serial2 serial3)],
2412 description
=> "opens a serial terminal (defaults to display)",
2417 additionalProperties
=> 0,
2419 user
=> { type
=> 'string' },
2420 ticket
=> { type
=> 'string' },
2421 port
=> { type
=> 'integer' },
2422 upid
=> { type
=> 'string' },
2428 my $rpcenv = PVE
::RPCEnvironment
::get
();
2430 my $authuser = $rpcenv->get_user();
2432 my $vmid = $param->{vmid
};
2433 my $node = $param->{node
};
2434 my $serial = $param->{serial
};
2436 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # check if VM exists
2438 if (!defined($serial)) {
2440 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2441 $serial = $vga->{type
} if $vga->{type
} =~ m/^serial\d+$/;
2445 my $authpath = "/vms/$vmid";
2447 my $ticket = PVE
::AccessControl
::assemble_vnc_ticket
($authuser, $authpath);
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 $remcmd = PVE
::SSHInfo
::ssh_info_to_command
($sshinfo, '-t');
2456 push @$remcmd, '--';
2458 $family = PVE
::Tools
::get_host_address_family
($node);
2461 my $port = PVE
::Tools
::next_vnc_port
($family);
2463 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-escape', '0'];
2464 push @$termcmd, '-iface', $serial if $serial;
2469 syslog
('info', "starting qemu termproxy $upid\n");
2471 my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath,
2472 '--perm', 'VM.Console', '--'];
2473 push @$cmd, @$remcmd, @$termcmd;
2475 PVE
::Tools
::run_command
($cmd);
2478 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
2480 PVE
::Tools
::wait_for_vnc_port
($port);
2490 __PACKAGE__-
>register_method({
2491 name
=> 'vncwebsocket',
2492 path
=> '{vmid}/vncwebsocket',
2495 description
=> "You also need to pass a valid ticket (vncticket).",
2496 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2498 description
=> "Opens a weksocket for VNC traffic.",
2500 additionalProperties
=> 0,
2502 node
=> get_standard_option
('pve-node'),
2503 vmid
=> get_standard_option
('pve-vmid'),
2505 description
=> "Ticket from previous call to vncproxy.",
2510 description
=> "Port number returned by previous vncproxy call.",
2520 port
=> { type
=> 'string' },
2526 my $rpcenv = PVE
::RPCEnvironment
::get
();
2528 my $authuser = $rpcenv->get_user();
2530 my $vmid = $param->{vmid
};
2531 my $node = $param->{node
};
2533 my $authpath = "/vms/$vmid";
2535 PVE
::AccessControl
::verify_vnc_ticket
($param->{vncticket
}, $authuser, $authpath);
2537 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # VM exists ?
2539 # Note: VNC ports are acessible from outside, so we do not gain any
2540 # security if we verify that $param->{port} belongs to VM $vmid. This
2541 # check is done by verifying the VNC ticket (inside VNC protocol).
2543 my $port = $param->{port
};
2545 return { port
=> $port };
2548 __PACKAGE__-
>register_method({
2549 name
=> 'spiceproxy',
2550 path
=> '{vmid}/spiceproxy',
2555 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2557 description
=> "Returns a SPICE configuration to connect to the VM.",
2559 additionalProperties
=> 0,
2561 node
=> get_standard_option
('pve-node'),
2562 vmid
=> get_standard_option
('pve-vmid'),
2563 proxy
=> get_standard_option
('spice-proxy', { optional
=> 1 }),
2566 returns
=> get_standard_option
('remote-viewer-config'),
2570 my $rpcenv = PVE
::RPCEnvironment
::get
();
2572 my $authuser = $rpcenv->get_user();
2574 my $vmid = $param->{vmid
};
2575 my $node = $param->{node
};
2576 my $proxy = $param->{proxy
};
2578 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node);
2579 my $title = "VM $vmid";
2580 $title .= " - ". $conf->{name
} if $conf->{name
};
2582 my $port = PVE
::QemuServer
::spice_port
($vmid);
2584 my ($ticket, undef, $remote_viewer_config) =
2585 PVE
::AccessControl
::remote_viewer_config
($authuser, $vmid, $node, $proxy, $title, $port);
2587 mon_cmd
($vmid, "set_password", protocol
=> 'spice', password
=> $ticket);
2588 mon_cmd
($vmid, "expire_password", protocol
=> 'spice', time => "+30");
2590 return $remote_viewer_config;
2593 __PACKAGE__-
>register_method({
2595 path
=> '{vmid}/status',
2598 description
=> "Directory index",
2603 additionalProperties
=> 0,
2605 node
=> get_standard_option
('pve-node'),
2606 vmid
=> get_standard_option
('pve-vmid'),
2614 subdir
=> { type
=> 'string' },
2617 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
2623 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
2626 { subdir
=> 'current' },
2627 { subdir
=> 'start' },
2628 { subdir
=> 'stop' },
2629 { subdir
=> 'reset' },
2630 { subdir
=> 'shutdown' },
2631 { subdir
=> 'suspend' },
2632 { subdir
=> 'reboot' },
2638 __PACKAGE__-
>register_method({
2639 name
=> 'vm_status',
2640 path
=> '{vmid}/status/current',
2643 protected
=> 1, # qemu pid files are only readable by root
2644 description
=> "Get virtual machine status.",
2646 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2649 additionalProperties
=> 0,
2651 node
=> get_standard_option
('pve-node'),
2652 vmid
=> get_standard_option
('pve-vmid'),
2658 %$PVE::QemuServer
::vmstatus_return_properties
,
2660 description
=> "HA manager service status.",
2664 description
=> "QEMU VGA configuration supports spice.",
2669 description
=> "QEMU Guest Agent is enabled in config.",
2679 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
2681 my $vmstatus = PVE
::QemuServer
::vmstatus
($param->{vmid
}, 1);
2682 my $status = $vmstatus->{$param->{vmid
}};
2684 $status->{ha
} = PVE
::HA
::Config
::get_service_status
("vm:$param->{vmid}");
2687 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2688 my $spice = defined($vga->{type
}) && $vga->{type
} =~ /^virtio/;
2689 $spice ||= PVE
::QemuServer
::vga_conf_has_spice
($conf->{vga
});
2690 $status->{spice
} = 1 if $spice;
2692 $status->{agent
} = 1 if PVE
::QemuServer
::get_qga_key
($conf, 'enabled');
2697 __PACKAGE__-
>register_method({
2699 path
=> '{vmid}/status/start',
2703 description
=> "Start virtual machine.",
2705 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2708 additionalProperties
=> 0,
2710 node
=> get_standard_option
('pve-node'),
2711 vmid
=> get_standard_option
('pve-vmid',
2712 { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
2713 skiplock
=> get_standard_option
('skiplock'),
2714 stateuri
=> get_standard_option
('pve-qm-stateuri'),
2715 migratedfrom
=> get_standard_option
('pve-node',{ optional
=> 1 }),
2718 enum
=> ['secure', 'insecure'],
2719 description
=> "Migration traffic is encrypted using an SSH " .
2720 "tunnel by default. On secure, completely private networks " .
2721 "this can be disabled to increase performance.",
2724 migration_network
=> {
2725 type
=> 'string', format
=> 'CIDR',
2726 description
=> "CIDR of the (sub) network that is used for migration.",
2729 machine
=> get_standard_option
('pve-qemu-machine'),
2731 description
=> "Override QEMU's -cpu argument with the given string.",
2735 targetstorage
=> get_standard_option
('pve-targetstorage'),
2737 description
=> "Wait maximal timeout seconds.",
2740 default => 'max(30, vm memory in GiB)',
2751 my $rpcenv = PVE
::RPCEnvironment
::get
();
2752 my $authuser = $rpcenv->get_user();
2754 my $node = extract_param
($param, 'node');
2755 my $vmid = extract_param
($param, 'vmid');
2756 my $timeout = extract_param
($param, 'timeout');
2757 my $machine = extract_param
($param, 'machine');
2759 my $get_root_param = sub {
2760 my $value = extract_param
($param, $_[0]);
2761 raise_param_exc
({ "$_[0]" => "Only root may use this option." })
2762 if $value && $authuser ne 'root@pam';
2766 my $stateuri = $get_root_param->('stateuri');
2767 my $skiplock = $get_root_param->('skiplock');
2768 my $migratedfrom = $get_root_param->('migratedfrom');
2769 my $migration_type = $get_root_param->('migration_type');
2770 my $migration_network = $get_root_param->('migration_network');
2771 my $targetstorage = $get_root_param->('targetstorage');
2772 my $force_cpu = $get_root_param->('force-cpu');
2776 if ($targetstorage) {
2777 raise_param_exc
({ targetstorage
=> "targetstorage can only by used with migratedfrom." })
2779 $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($targetstorage, 'pve-storage-id') };
2780 raise_param_exc
({ targetstorage
=> "failed to parse storage map: $@" })
2784 # read spice ticket from STDIN
2786 my $nbd_protocol_version = 0;
2787 my $replicated_volumes = {};
2788 my $offline_volumes = {};
2789 if ($stateuri && ($stateuri eq 'tcp' || $stateuri eq 'unix') && $migratedfrom && ($rpcenv->{type
} eq 'cli')) {
2790 while (defined(my $line = <STDIN
>)) {
2792 if ($line =~ m/^spice_ticket: (.+)$/) {
2794 } elsif ($line =~ m/^nbd_protocol_version: (\d+)$/) {
2795 $nbd_protocol_version = $1;
2796 } elsif ($line =~ m/^replicated_volume: (.*)$/) {
2797 $replicated_volumes->{$1} = 1;
2798 } elsif ($line =~ m/^tpmstate0: (.*)$/) { # Deprecated, use offline_volume instead
2799 $offline_volumes->{tpmstate0
} = $1;
2800 } elsif ($line =~ m/^offline_volume: ([^:]+): (.*)$/) {
2801 $offline_volumes->{$1} = $2;
2802 } elsif (!$spice_ticket) {
2803 # fallback for old source node
2804 $spice_ticket = $line;
2806 warn "unknown 'start' parameter on STDIN: '$line'\n";
2811 PVE
::Cluster
::check_cfs_quorum
();
2813 my $storecfg = PVE
::Storage
::config
();
2815 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && !$stateuri && $rpcenv->{type
} ne 'ha') {
2819 print "Requesting HA start for VM $vmid\n";
2821 my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'started'];
2822 PVE
::Tools
::run_command
($cmd);
2826 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
2833 syslog
('info', "start VM $vmid: $upid\n");
2835 my $migrate_opts = {
2836 migratedfrom
=> $migratedfrom,
2837 spice_ticket
=> $spice_ticket,
2838 network
=> $migration_network,
2839 type
=> $migration_type,
2840 storagemap
=> $storagemap,
2841 nbd_proto_version
=> $nbd_protocol_version,
2842 replicated_volumes
=> $replicated_volumes,
2843 offline_volumes
=> $offline_volumes,
2847 statefile
=> $stateuri,
2848 skiplock
=> $skiplock,
2849 forcemachine
=> $machine,
2850 timeout
=> $timeout,
2851 forcecpu
=> $force_cpu,
2854 PVE
::QemuServer
::vm_start
($storecfg, $vmid, $params, $migrate_opts);
2858 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
2862 __PACKAGE__-
>register_method({
2864 path
=> '{vmid}/status/stop',
2868 description
=> "Stop virtual machine. The qemu process will exit immediately. This" .
2869 "is akin to pulling the power plug of a running computer and may damage the VM data",
2871 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2874 additionalProperties
=> 0,
2876 node
=> get_standard_option
('pve-node'),
2877 vmid
=> get_standard_option
('pve-vmid',
2878 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
2879 skiplock
=> get_standard_option
('skiplock'),
2880 migratedfrom
=> get_standard_option
('pve-node', { optional
=> 1 }),
2882 description
=> "Wait maximal timeout seconds.",
2888 description
=> "Do not deactivate storage volumes.",
2901 my $rpcenv = PVE
::RPCEnvironment
::get
();
2902 my $authuser = $rpcenv->get_user();
2904 my $node = extract_param
($param, 'node');
2905 my $vmid = extract_param
($param, 'vmid');
2907 my $skiplock = extract_param
($param, 'skiplock');
2908 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2909 if $skiplock && $authuser ne 'root@pam';
2911 my $keepActive = extract_param
($param, 'keepActive');
2912 raise_param_exc
({ keepActive
=> "Only root may use this option." })
2913 if $keepActive && $authuser ne 'root@pam';
2915 my $migratedfrom = extract_param
($param, 'migratedfrom');
2916 raise_param_exc
({ migratedfrom
=> "Only root may use this option." })
2917 if $migratedfrom && $authuser ne 'root@pam';
2920 my $storecfg = PVE
::Storage
::config
();
2922 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && ($rpcenv->{type
} ne 'ha') && !defined($migratedfrom)) {
2927 print "Requesting HA stop for VM $vmid\n";
2929 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", '0'];
2930 PVE
::Tools
::run_command
($cmd);
2934 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2940 syslog
('info', "stop VM $vmid: $upid\n");
2942 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0,
2943 $param->{timeout
}, 0, 1, $keepActive, $migratedfrom);
2947 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
2951 __PACKAGE__-
>register_method({
2953 path
=> '{vmid}/status/reset',
2957 description
=> "Reset virtual machine.",
2959 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2962 additionalProperties
=> 0,
2964 node
=> get_standard_option
('pve-node'),
2965 vmid
=> get_standard_option
('pve-vmid',
2966 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
2967 skiplock
=> get_standard_option
('skiplock'),
2976 my $rpcenv = PVE
::RPCEnvironment
::get
();
2978 my $authuser = $rpcenv->get_user();
2980 my $node = extract_param
($param, 'node');
2982 my $vmid = extract_param
($param, 'vmid');
2984 my $skiplock = extract_param
($param, 'skiplock');
2985 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2986 if $skiplock && $authuser ne 'root@pam';
2988 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
2993 PVE
::QemuServer
::vm_reset
($vmid, $skiplock);
2998 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
3001 __PACKAGE__-
>register_method({
3002 name
=> 'vm_shutdown',
3003 path
=> '{vmid}/status/shutdown',
3007 description
=> "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
3008 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
3010 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3013 additionalProperties
=> 0,
3015 node
=> get_standard_option
('pve-node'),
3016 vmid
=> get_standard_option
('pve-vmid',
3017 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3018 skiplock
=> get_standard_option
('skiplock'),
3020 description
=> "Wait maximal timeout seconds.",
3026 description
=> "Make sure the VM stops.",
3032 description
=> "Do not deactivate storage volumes.",
3045 my $rpcenv = PVE
::RPCEnvironment
::get
();
3046 my $authuser = $rpcenv->get_user();
3048 my $node = extract_param
($param, 'node');
3049 my $vmid = extract_param
($param, 'vmid');
3051 my $skiplock = extract_param
($param, 'skiplock');
3052 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3053 if $skiplock && $authuser ne 'root@pam';
3055 my $keepActive = extract_param
($param, 'keepActive');
3056 raise_param_exc
({ keepActive
=> "Only root may use this option." })
3057 if $keepActive && $authuser ne 'root@pam';
3059 my $storecfg = PVE
::Storage
::config
();
3063 # if vm is paused, do not shutdown (but stop if forceStop = 1)
3064 # otherwise, we will infer a shutdown command, but run into the timeout,
3065 # then when the vm is resumed, it will instantly shutdown
3067 # checking the qmp status here to get feedback to the gui/cli/api
3068 # and the status query should not take too long
3069 if (PVE
::QemuServer
::vm_is_paused
($vmid)) {
3070 if ($param->{forceStop
}) {
3071 warn "VM is paused - stop instead of shutdown\n";
3074 die "VM is paused - cannot shutdown\n";
3078 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
3080 my $timeout = $param->{timeout
} // 60;
3084 print "Requesting HA stop for VM $vmid\n";
3086 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", "$timeout"];
3087 PVE
::Tools
::run_command
($cmd);
3091 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
3098 syslog
('info', "shutdown VM $vmid: $upid\n");
3100 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0, $param->{timeout
},
3101 $shutdown, $param->{forceStop
}, $keepActive);
3105 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
3109 __PACKAGE__-
>register_method({
3110 name
=> 'vm_reboot',
3111 path
=> '{vmid}/status/reboot',
3115 description
=> "Reboot the VM by shutting it down, and starting it again. Applies pending changes.",
3117 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3120 additionalProperties
=> 0,
3122 node
=> get_standard_option
('pve-node'),
3123 vmid
=> get_standard_option
('pve-vmid',
3124 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3126 description
=> "Wait maximal timeout seconds for the shutdown.",
3139 my $rpcenv = PVE
::RPCEnvironment
::get
();
3140 my $authuser = $rpcenv->get_user();
3142 my $node = extract_param
($param, 'node');
3143 my $vmid = extract_param
($param, 'vmid');
3145 die "VM is paused - cannot shutdown\n" if PVE
::QemuServer
::vm_is_paused
($vmid);
3147 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3152 syslog
('info', "requesting reboot of VM $vmid: $upid\n");
3153 PVE
::QemuServer
::vm_reboot
($vmid, $param->{timeout
});
3157 return $rpcenv->fork_worker('qmreboot', $vmid, $authuser, $realcmd);
3160 __PACKAGE__-
>register_method({
3161 name
=> 'vm_suspend',
3162 path
=> '{vmid}/status/suspend',
3166 description
=> "Suspend virtual machine.",
3168 description
=> "You need 'VM.PowerMgmt' on /vms/{vmid}, and if you have set 'todisk',".
3169 " you need also 'VM.Config.Disk' on /vms/{vmid} and 'Datastore.AllocateSpace'".
3170 " on the storage for the vmstate.",
3171 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3174 additionalProperties
=> 0,
3176 node
=> get_standard_option
('pve-node'),
3177 vmid
=> get_standard_option
('pve-vmid',
3178 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3179 skiplock
=> get_standard_option
('skiplock'),
3184 description
=> 'If set, suspends the VM to disk. Will be resumed on next VM start.',
3186 statestorage
=> get_standard_option
('pve-storage-id', {
3187 description
=> "The storage for the VM state",
3188 requires
=> 'todisk',
3190 completion
=> \
&PVE
::Storage
::complete_storage_enabled
,
3200 my $rpcenv = PVE
::RPCEnvironment
::get
();
3201 my $authuser = $rpcenv->get_user();
3203 my $node = extract_param
($param, 'node');
3204 my $vmid = extract_param
($param, 'vmid');
3206 my $todisk = extract_param
($param, 'todisk') // 0;
3208 my $statestorage = extract_param
($param, 'statestorage');
3210 my $skiplock = extract_param
($param, 'skiplock');
3211 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3212 if $skiplock && $authuser ne 'root@pam';
3214 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3216 die "Cannot suspend HA managed VM to disk\n"
3217 if $todisk && PVE
::HA
::Config
::vm_is_ha_managed
($vmid);
3219 # early check for storage permission, for better user feedback
3221 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
3222 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3224 # cannot save the state of a non-virtualized PCIe device, so resume cannot really work
3225 for my $key (keys %$conf) {
3226 next if $key !~ /^hostpci\d+/;
3227 die "cannot suspend VM to disk due to passed-through PCI device(s), which lack the"
3228 ." possibility to save/restore their internal state\n";
3231 if (!$statestorage) {
3232 # get statestorage from config if none is given
3233 my $storecfg = PVE
::Storage
::config
();
3234 $statestorage = PVE
::QemuServer
::find_vmstate_storage
($conf, $storecfg);
3237 $rpcenv->check($authuser, "/storage/$statestorage", ['Datastore.AllocateSpace']);
3243 syslog
('info', "suspend VM $vmid: $upid\n");
3245 PVE
::QemuServer
::vm_suspend
($vmid, $skiplock, $todisk, $statestorage);
3250 my $taskname = $todisk ?
'qmsuspend' : 'qmpause';
3251 return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd);
3254 __PACKAGE__-
>register_method({
3255 name
=> 'vm_resume',
3256 path
=> '{vmid}/status/resume',
3260 description
=> "Resume virtual machine.",
3262 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3265 additionalProperties
=> 0,
3267 node
=> get_standard_option
('pve-node'),
3268 vmid
=> get_standard_option
('pve-vmid',
3269 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3270 skiplock
=> get_standard_option
('skiplock'),
3271 nocheck
=> { type
=> 'boolean', optional
=> 1 },
3281 my $rpcenv = PVE
::RPCEnvironment
::get
();
3283 my $authuser = $rpcenv->get_user();
3285 my $node = extract_param
($param, 'node');
3287 my $vmid = extract_param
($param, 'vmid');
3289 my $skiplock = extract_param
($param, 'skiplock');
3290 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3291 if $skiplock && $authuser ne 'root@pam';
3293 # nocheck is used as part of migration when config file might be still
3295 my $nocheck = extract_param
($param, 'nocheck');
3296 raise_param_exc
({ nocheck
=> "Only root may use this option." })
3297 if $nocheck && $authuser ne 'root@pam';
3299 my $to_disk_suspended;
3301 PVE
::QemuConfig-
>lock_config($vmid, sub {
3302 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3303 $to_disk_suspended = PVE
::QemuConfig-
>has_lock($conf, 'suspended');
3307 die "VM $vmid not running\n"
3308 if !$to_disk_suspended && !PVE
::QemuServer
::check_running
($vmid, $nocheck);
3313 syslog
('info', "resume VM $vmid: $upid\n");
3315 if (!$to_disk_suspended) {
3316 PVE
::QemuServer
::vm_resume
($vmid, $skiplock, $nocheck);
3318 my $storecfg = PVE
::Storage
::config
();
3319 PVE
::QemuServer
::vm_start
($storecfg, $vmid, { skiplock
=> $skiplock });
3325 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
3328 __PACKAGE__-
>register_method({
3329 name
=> 'vm_sendkey',
3330 path
=> '{vmid}/sendkey',
3334 description
=> "Send key event to virtual machine.",
3336 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
3339 additionalProperties
=> 0,
3341 node
=> get_standard_option
('pve-node'),
3342 vmid
=> get_standard_option
('pve-vmid',
3343 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3344 skiplock
=> get_standard_option
('skiplock'),
3346 description
=> "The key (qemu monitor encoding).",
3351 returns
=> { type
=> 'null'},
3355 my $rpcenv = PVE
::RPCEnvironment
::get
();
3357 my $authuser = $rpcenv->get_user();
3359 my $node = extract_param
($param, 'node');
3361 my $vmid = extract_param
($param, 'vmid');
3363 my $skiplock = extract_param
($param, 'skiplock');
3364 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3365 if $skiplock && $authuser ne 'root@pam';
3367 PVE
::QemuServer
::vm_sendkey
($vmid, $skiplock, $param->{key
});
3372 __PACKAGE__-
>register_method({
3373 name
=> 'vm_feature',
3374 path
=> '{vmid}/feature',
3378 description
=> "Check if feature for virtual machine is available.",
3380 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3383 additionalProperties
=> 0,
3385 node
=> get_standard_option
('pve-node'),
3386 vmid
=> get_standard_option
('pve-vmid'),
3388 description
=> "Feature to check.",
3390 enum
=> [ 'snapshot', 'clone', 'copy' ],
3392 snapname
=> get_standard_option
('pve-snapshot-name', {
3400 hasFeature
=> { type
=> 'boolean' },
3403 items
=> { type
=> 'string' },
3410 my $node = extract_param
($param, 'node');
3412 my $vmid = extract_param
($param, 'vmid');
3414 my $snapname = extract_param
($param, 'snapname');
3416 my $feature = extract_param
($param, 'feature');
3418 my $running = PVE
::QemuServer
::check_running
($vmid);
3420 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3423 my $snap = $conf->{snapshots
}->{$snapname};
3424 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3427 my $storecfg = PVE
::Storage
::config
();
3429 my $nodelist = PVE
::QemuServer
::shared_nodes
($conf, $storecfg);
3430 my $hasFeature = PVE
::QemuConfig-
>has_feature($feature, $conf, $storecfg, $snapname, $running);
3433 hasFeature
=> $hasFeature,
3434 nodes
=> [ keys %$nodelist ],
3438 __PACKAGE__-
>register_method({
3440 path
=> '{vmid}/clone',
3444 description
=> "Create a copy of virtual machine/template.",
3446 description
=> "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
3447 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
3448 "'Datastore.AllocateSpace' on any used storage and 'SDN.Use' on any used bridge/vnet",
3451 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
3453 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
3454 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param
=> 'pool'],
3459 additionalProperties
=> 0,
3461 node
=> get_standard_option
('pve-node'),
3462 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3463 newid
=> get_standard_option
('pve-vmid', {
3464 completion
=> \
&PVE
::Cluster
::complete_next_vmid
,
3465 description
=> 'VMID for the clone.' }),
3468 type
=> 'string', format
=> 'dns-name',
3469 description
=> "Set a name for the new VM.",
3474 description
=> "Description for the new VM.",
3478 type
=> 'string', format
=> 'pve-poolid',
3479 description
=> "Add the new VM to the specified pool.",
3481 snapname
=> get_standard_option
('pve-snapshot-name', {
3484 storage
=> get_standard_option
('pve-storage-id', {
3485 description
=> "Target storage for full clone.",
3489 description
=> "Target format for file storage. Only valid for full clone.",
3492 enum
=> [ 'raw', 'qcow2', 'vmdk'],
3497 description
=> "Create a full copy of all disks. This is always done when " .
3498 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
3500 target
=> get_standard_option
('pve-node', {
3501 description
=> "Target node. Only allowed if the original VM is on shared storage.",
3505 description
=> "Override I/O bandwidth limit (in KiB/s).",
3509 default => 'clone limit from datacenter or storage config',
3519 my $rpcenv = PVE
::RPCEnvironment
::get
();
3520 my $authuser = $rpcenv->get_user();
3522 my $node = extract_param
($param, 'node');
3523 my $vmid = extract_param
($param, 'vmid');
3524 my $newid = extract_param
($param, 'newid');
3525 my $pool = extract_param
($param, 'pool');
3527 my $snapname = extract_param
($param, 'snapname');
3528 my $storage = extract_param
($param, 'storage');
3529 my $format = extract_param
($param, 'format');
3530 my $target = extract_param
($param, 'target');
3532 my $localnode = PVE
::INotify
::nodename
();
3534 if ($target && ($target eq $localnode || $target eq 'localhost')) {
3538 my $running = PVE
::QemuServer
::check_running
($vmid) || 0;
3540 my $load_and_check = sub {
3541 $rpcenv->check_pool_exist($pool) if defined($pool);
3542 PVE
::Cluster
::check_node_exists
($target) if $target;
3544 my $storecfg = PVE
::Storage
::config
();
3547 # check if storage is enabled on local node
3548 PVE
::Storage
::storage_check_enabled
($storecfg, $storage);
3550 # check if storage is available on target node
3551 PVE
::Storage
::storage_check_enabled
($storecfg, $storage, $target);
3552 # clone only works if target storage is shared
3553 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storage);
3554 die "can't clone to non-shared storage '$storage'\n"
3555 if !$scfg->{shared
};
3559 PVE
::Cluster
::check_cfs_quorum
();
3561 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3562 PVE
::QemuConfig-
>check_lock($conf);
3564 my $verify_running = PVE
::QemuServer
::check_running
($vmid) || 0;
3565 die "unexpected state change\n" if $verify_running != $running;
3567 die "snapshot '$snapname' does not exist\n"
3568 if $snapname && !defined( $conf->{snapshots
}->{$snapname});
3570 my $full = $param->{full
} // !PVE
::QemuConfig-
>is_template($conf);
3572 die "parameter 'storage' not allowed for linked clones\n"
3573 if defined($storage) && !$full;
3575 die "parameter 'format' not allowed for linked clones\n"
3576 if defined($format) && !$full;
3578 my $oldconf = $snapname ?
$conf->{snapshots
}->{$snapname} : $conf;
3580 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
3581 PVE
::QemuServer
::check_mapping_access
($rpcenv, $authuser, $oldconf);
3583 PVE
::QemuServer
::check_bridge_access
($rpcenv, $authuser, $oldconf);
3585 die "can't clone VM to node '$target' (VM uses local storage)\n"
3586 if $target && !$sharedvm;
3588 my $conffile = PVE
::QemuConfig-
>config_file($newid);
3589 die "unable to create VM $newid: config file already exists\n"
3592 my $newconf = { lock => 'clone' };
3597 foreach my $opt (keys %$oldconf) {
3598 my $value = $oldconf->{$opt};
3600 # do not copy snapshot related info
3601 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
3602 $opt eq 'vmstate' || $opt eq 'snapstate';
3604 # no need to copy unused images, because VMID(owner) changes anyways
3605 next if $opt =~ m/^unused\d+$/;
3607 die "cannot clone TPM state while VM is running\n"
3608 if $full && $running && !$snapname && $opt eq 'tpmstate0';
3610 # always change MAC! address
3611 if ($opt =~ m/^net(\d+)$/) {
3612 my $net = PVE
::QemuServer
::parse_net
($value);
3613 my $dc = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
3614 $net->{macaddr
} = PVE
::Tools
::random_ether_addr
($dc->{mac_prefix
});
3615 $newconf->{$opt} = PVE
::QemuServer
::print_net
($net);
3616 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
3617 my $drive = PVE
::QemuServer
::parse_drive
($opt, $value);
3618 die "unable to parse drive options for '$opt'\n" if !$drive;
3619 if (PVE
::QemuServer
::drive_is_cdrom
($drive, 1)) {
3620 $newconf->{$opt} = $value; # simply copy configuration
3622 if ($full || PVE
::QemuServer
::drive_is_cloudinit
($drive)) {
3623 die "Full clone feature is not supported for drive '$opt'\n"
3624 if !PVE
::Storage
::volume_has_feature
($storecfg, 'copy', $drive->{file
}, $snapname, $running);
3625 $fullclone->{$opt} = 1;
3627 # not full means clone instead of copy
3628 die "Linked clone feature is not supported for drive '$opt'\n"
3629 if !PVE
::Storage
::volume_has_feature
($storecfg, 'clone', $drive->{file
}, $snapname, $running);
3631 $drives->{$opt} = $drive;
3632 next if PVE
::QemuServer
::drive_is_cloudinit
($drive);
3633 push @$vollist, $drive->{file
};
3636 # copy everything else
3637 $newconf->{$opt} = $value;
3641 return ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone);
3645 my ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone) = $load_and_check->();
3646 my $storecfg = PVE
::Storage
::config
();
3648 # auto generate a new uuid
3649 my $smbios1 = PVE
::QemuServer
::parse_smbios1
($newconf->{smbios1
} || '');
3650 $smbios1->{uuid
} = PVE
::QemuServer
::generate_uuid
();
3651 $newconf->{smbios1
} = PVE
::QemuServer
::print_smbios1
($smbios1);
3652 # auto generate a new vmgenid only if the option was set for template
3653 if ($newconf->{vmgenid
}) {
3654 $newconf->{vmgenid
} = PVE
::QemuServer
::generate_uuid
();
3657 delete $newconf->{template
};
3659 if ($param->{name
}) {
3660 $newconf->{name
} = $param->{name
};
3662 $newconf->{name
} = "Copy-of-VM-" . ($oldconf->{name
} // $vmid);
3665 if ($param->{description
}) {
3666 $newconf->{description
} = $param->{description
};
3669 # create empty/temp config - this fails if VM already exists on other node
3670 # FIXME use PVE::QemuConfig->create_and_lock_config and adapt code
3671 PVE
::Tools
::file_set_contents
($conffile, "# qmclone temporary file\nlock: clone\n");
3673 PVE
::Firewall
::clone_vmfw_conf
($vmid, $newid);
3675 my $newvollist = [];
3682 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
3684 PVE
::Storage
::activate_volumes
($storecfg, $vollist, $snapname);
3686 my $bwlimit = extract_param
($param, 'bwlimit');
3688 my $total_jobs = scalar(keys %{$drives});
3691 foreach my $opt (sort keys %$drives) {
3692 my $drive = $drives->{$opt};
3693 my $skipcomplete = ($total_jobs != $i); # finish after last drive
3694 my $completion = $skipcomplete ?
'skip' : 'complete';
3696 my $src_sid = PVE
::Storage
::parse_volume_id
($drive->{file
});
3697 my $storage_list = [ $src_sid ];
3698 push @$storage_list, $storage if defined($storage);
3699 my $clonelimit = PVE
::Storage
::get_bandwidth_limit
('clone', $storage_list, $bwlimit);
3703 running
=> $running,
3706 snapname
=> $snapname,
3712 storage
=> $storage,
3716 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($oldconf)
3717 if $opt eq 'efidisk0';
3719 my $newdrive = PVE
::QemuServer
::clone_disk
(
3731 $newconf->{$opt} = PVE
::QemuServer
::print_drive
($newdrive);
3733 PVE
::QemuConfig-
>write_config($newid, $newconf);
3737 delete $newconf->{lock};
3739 # do not write pending changes
3740 if (my @changes = keys %{$newconf->{pending
}}) {
3741 my $pending = join(',', @changes);
3742 warn "found pending changes for '$pending', discarding for clone\n";
3743 delete $newconf->{pending
};
3746 PVE
::QemuConfig-
>write_config($newid, $newconf);
3749 # always deactivate volumes - avoid lvm LVs to be active on several nodes
3750 PVE
::Storage
::deactivate_volumes
($storecfg, $vollist, $snapname) if !$running;
3751 PVE
::Storage
::deactivate_volumes
($storecfg, $newvollist);
3753 my $newconffile = PVE
::QemuConfig-
>config_file($newid, $target);
3754 die "Failed to move config to node '$target' - rename failed: $!\n"
3755 if !rename($conffile, $newconffile);
3758 PVE
::AccessControl
::add_vm_to_pool
($newid, $pool) if $pool;
3761 eval { PVE
::QemuServer
::qemu_blockjobs_cancel
($vmid, $jobs) };
3762 sleep 1; # some storage like rbd need to wait before release volume - really?
3764 foreach my $volid (@$newvollist) {
3765 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
3769 PVE
::Firewall
::remove_vmfw_conf
($newid);
3771 unlink $conffile; # avoid races -> last thing before die
3773 die "clone failed: $err";
3779 # Aquire exclusive lock lock for $newid
3780 my $lock_target_vm = sub {
3781 return PVE
::QemuConfig-
>lock_config_full($newid, 1, $clonefn);
3784 my $lock_source_vm = sub {
3785 # exclusive lock if VM is running - else shared lock is enough;
3787 return PVE
::QemuConfig-
>lock_config_full($vmid, 1, $lock_target_vm);
3789 return PVE
::QemuConfig-
>lock_config_shared($vmid, 1, $lock_target_vm);
3793 $load_and_check->(); # early checks before forking/locking
3795 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $lock_source_vm);
3798 __PACKAGE__-
>register_method({
3799 name
=> 'move_vm_disk',
3800 path
=> '{vmid}/move_disk',
3804 description
=> "Move volume to different storage or to a different VM.",
3806 description
=> "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
3807 "and 'Datastore.AllocateSpace' permissions on the storage. To move ".
3808 "a disk to another VM, you need the permissions on the target VM as well.",
3809 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
3812 additionalProperties
=> 0,
3814 node
=> get_standard_option
('pve-node'),
3815 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3816 'target-vmid' => get_standard_option
('pve-vmid', {
3817 completion
=> \
&PVE
::QemuServer
::complete_vmid
,
3822 description
=> "The disk you want to move.",
3823 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
3825 storage
=> get_standard_option
('pve-storage-id', {
3826 description
=> "Target storage.",
3827 completion
=> \
&PVE
::QemuServer
::complete_storage
,
3832 description
=> "Target Format.",
3833 enum
=> [ 'raw', 'qcow2', 'vmdk' ],
3838 description
=> "Delete the original disk after successful copy. By default the"
3839 ." original disk is kept as unused disk.",
3845 description
=> 'Prevent changes if current configuration file has different SHA1"
3846 ." digest. This can be used to prevent concurrent modifications.',
3851 description
=> "Override I/O bandwidth limit (in KiB/s).",
3855 default => 'move limit from datacenter or storage config',
3859 description
=> "The config key the disk will be moved to on the target VM"
3860 ." (for example, ide0 or scsi1). Default is the source disk key.",
3861 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
3864 'target-digest' => {
3866 description
=> 'Prevent changes if the current config file of the target VM has a"
3867 ." different SHA1 digest. This can be used to detect concurrent modifications.',
3875 description
=> "the task ID.",
3880 my $rpcenv = PVE
::RPCEnvironment
::get
();
3881 my $authuser = $rpcenv->get_user();
3883 my $node = extract_param
($param, 'node');
3884 my $vmid = extract_param
($param, 'vmid');
3885 my $target_vmid = extract_param
($param, 'target-vmid');
3886 my $digest = extract_param
($param, 'digest');
3887 my $target_digest = extract_param
($param, 'target-digest');
3888 my $disk = extract_param
($param, 'disk');
3889 my $target_disk = extract_param
($param, 'target-disk') // $disk;
3890 my $storeid = extract_param
($param, 'storage');
3891 my $format = extract_param
($param, 'format');
3893 my $storecfg = PVE
::Storage
::config
();
3895 my $load_and_check_move = sub {
3896 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3897 PVE
::QemuConfig-
>check_lock($conf);
3899 PVE
::Tools
::assert_if_modified
($digest, $conf->{digest
});
3901 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3903 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
3905 die "disk '$disk' has no associated volume\n" if !$drive->{file
};
3906 die "you can't move a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
3908 my $old_volid = $drive->{file
};
3910 my ($oldstoreid, $oldvolname) = PVE
::Storage
::parse_volume_id
($old_volid);
3911 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
3915 die "you can't move to the same storage with same format\n"
3916 if $oldstoreid eq $storeid && (!$format || !$oldfmt || $oldfmt eq $format);
3918 # this only checks snapshots because $disk is passed!
3919 my $snapshotted = PVE
::QemuServer
::Drive
::is_volume_in_use
(
3925 die "you can't move a disk with snapshots and delete the source\n"
3926 if $snapshotted && $param->{delete};
3928 return ($conf, $drive, $oldstoreid, $snapshotted);
3931 my $move_updatefn = sub {
3932 my ($conf, $drive, $oldstoreid, $snapshotted) = $load_and_check_move->();
3933 my $old_volid = $drive->{file
};
3935 PVE
::Cluster
::log_msg
(
3938 "move disk VM $vmid: move --disk $disk --storage $storeid"
3941 my $running = PVE
::QemuServer
::check_running
($vmid);
3943 PVE
::Storage
::activate_volumes
($storecfg, [ $drive->{file
} ]);
3945 my $newvollist = [];
3951 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
3953 warn "moving disk with snapshots, snapshots will not be moved!\n"
3956 my $bwlimit = extract_param
($param, 'bwlimit');
3957 my $movelimit = PVE
::Storage
::get_bandwidth_limit
(
3959 [$oldstoreid, $storeid],
3965 running
=> $running,
3974 storage
=> $storeid,
3978 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($conf)
3979 if $disk eq 'efidisk0';
3981 my $newdrive = PVE
::QemuServer
::clone_disk
(
3992 $conf->{$disk} = PVE
::QemuServer
::print_drive
($newdrive);
3994 PVE
::QemuConfig-
>add_unused_volume($conf, $old_volid) if !$param->{delete};
3996 # convert moved disk to base if part of template
3997 PVE
::QemuServer
::template_create
($vmid, $conf, $disk)
3998 if PVE
::QemuConfig-
>is_template($conf);
4000 PVE
::QemuConfig-
>write_config($vmid, $conf);
4002 my $do_trim = PVE
::QemuServer
::get_qga_key
($conf, 'fstrim_cloned_disks');
4003 if ($running && $do_trim && PVE
::QemuServer
::qga_check_running
($vmid)) {
4004 eval { mon_cmd
($vmid, "guest-fstrim") };
4008 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
4009 PVE
::Storage
::deactivate_volumes
($storecfg, [ $newdrive->{file
} ])
4015 foreach my $volid (@$newvollist) {
4016 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid) };
4019 die "storage migration failed: $err";
4022 if ($param->{delete}) {
4024 PVE
::Storage
::deactivate_volumes
($storecfg, [$old_volid]);
4025 PVE
::Storage
::vdisk_free
($storecfg, $old_volid);
4031 my $load_and_check_reassign_configs = sub {
4032 my $vmlist = PVE
::Cluster
::get_vmlist
()->{ids
};
4034 die "could not find VM ${vmid}\n" if !exists($vmlist->{$vmid});
4035 die "could not find target VM ${target_vmid}\n" if !exists($vmlist->{$target_vmid});
4037 my $source_node = $vmlist->{$vmid}->{node
};
4038 my $target_node = $vmlist->{$target_vmid}->{node
};
4040 die "Both VMs need to be on the same node ($source_node != $target_node)\n"
4041 if $source_node ne $target_node;
4043 my $source_conf = PVE
::QemuConfig-
>load_config($vmid);
4044 PVE
::QemuConfig-
>check_lock($source_conf);
4045 my $target_conf = PVE
::QemuConfig-
>load_config($target_vmid);
4046 PVE
::QemuConfig-
>check_lock($target_conf);
4048 die "Can't move disks from or to template VMs\n"
4049 if ($source_conf->{template
} || $target_conf->{template
});
4052 eval { PVE
::Tools
::assert_if_modified
($digest, $source_conf->{digest
}) };
4053 die "VM ${vmid}: $@" if $@;
4056 if ($target_digest) {
4057 eval { PVE
::Tools
::assert_if_modified
($target_digest, $target_conf->{digest
}) };
4058 die "VM ${target_vmid}: $@" if $@;
4061 die "Disk '${disk}' for VM '$vmid' does not exist\n" if !defined($source_conf->{$disk});
4063 die "Target disk key '${target_disk}' is already in use for VM '$target_vmid'\n"
4064 if $target_conf->{$target_disk};
4066 my $drive = PVE
::QemuServer
::parse_drive
(
4068 $source_conf->{$disk},
4070 die "failed to parse source disk - $@\n" if !$drive;
4072 my $source_volid = $drive->{file
};
4074 die "disk '${disk}' has no associated volume\n" if !$source_volid;
4075 die "CD drive contents can't be moved to another VM\n"
4076 if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
4078 my $storeid = PVE
::Storage
::parse_volume_id
($source_volid, 1);
4079 die "Volume '$source_volid' not managed by PVE\n" if !defined($storeid);
4081 die "Can't move disk used by a snapshot to another VM\n"
4082 if PVE
::QemuServer
::Drive
::is_volume_in_use
($storecfg, $source_conf, $disk, $source_volid);
4083 die "Storage does not support moving of this disk to another VM\n"
4084 if (!PVE
::Storage
::volume_has_feature
($storecfg, 'rename', $source_volid));
4085 die "Cannot move disk to another VM while the source VM is running - detach first\n"
4086 if PVE
::QemuServer
::check_running
($vmid) && $disk !~ m/^unused\d+$/;
4088 # now re-parse using target disk slot format
4089 if ($target_disk =~ /^unused\d+$/) {
4090 $drive = PVE
::QemuServer
::parse_drive
(
4095 $drive = PVE
::QemuServer
::parse_drive
(
4097 $source_conf->{$disk},
4100 die "failed to parse source disk for target disk format - $@\n" if !$drive;
4102 my $repl_conf = PVE
::ReplicationConfig-
>new();
4103 if ($repl_conf->check_for_existing_jobs($target_vmid, 1)) {
4104 my $format = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4105 die "Cannot move disk to a replicated VM. Storage does not support replication!\n"
4106 if !PVE
::Storage
::storage_can_replicate
($storecfg, $storeid, $format);
4109 return ($source_conf, $target_conf, $drive);
4114 print STDERR
"$msg\n";
4117 my $disk_reassignfn = sub {
4118 return PVE
::QemuConfig-
>lock_config($vmid, sub {
4119 return PVE
::QemuConfig-
>lock_config($target_vmid, sub {
4120 my ($source_conf, $target_conf, $drive) = &$load_and_check_reassign_configs();
4122 my $source_volid = $drive->{file
};
4124 print "moving disk '$disk' from VM '$vmid' to '$target_vmid'\n";
4125 my ($storeid, $source_volname) = PVE
::Storage
::parse_volume_id
($source_volid);
4127 my $fmt = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4129 my $new_volid = PVE
::Storage
::rename_volume
(
4135 $drive->{file
} = $new_volid;
4137 my $boot_order = PVE
::QemuServer
::device_bootorder
($source_conf);
4138 if (defined(delete $boot_order->{$disk})) {
4139 print "removing disk '$disk' from boot order config\n";
4140 my $boot_devs = [ sort { $boot_order->{$a} <=> $boot_order->{$b} } keys %$boot_order ];
4141 $source_conf->{boot
} = PVE
::QemuServer
::print_bootorder
($boot_devs);
4144 delete $source_conf->{$disk};
4145 print "removing disk '${disk}' from VM '${vmid}' config\n";
4146 PVE
::QemuConfig-
>write_config($vmid, $source_conf);
4148 my $drive_string = PVE
::QemuServer
::print_drive
($drive);
4150 if ($target_disk =~ /^unused\d+$/) {
4151 $target_conf->{$target_disk} = $drive_string;
4152 PVE
::QemuConfig-
>write_config($target_vmid, $target_conf);
4157 vmid
=> $target_vmid,
4158 digest
=> $target_digest,
4159 $target_disk => $drive_string,
4165 # remove possible replication snapshots
4166 if (PVE
::Storage
::volume_has_feature
(
4172 PVE
::Replication
::prepare
(
4182 print "Failed to remove replication snapshots on moved disk " .
4183 "'$target_disk'. Manual cleanup could be necessary.\n";
4190 if ($target_vmid && $storeid) {
4191 my $msg = "either set 'storage' or 'target-vmid', but not both";
4192 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4193 } elsif ($target_vmid) {
4194 $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
4195 if $authuser ne 'root@pam';
4197 raise_param_exc
({ 'target-vmid' => "must be different than source VMID to reassign disk" })
4198 if $vmid eq $target_vmid;
4200 my (undef, undef, $drive) = &$load_and_check_reassign_configs();
4201 my $storage = PVE
::Storage
::parse_volume_id
($drive->{file
});
4202 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
4204 return $rpcenv->fork_worker(
4206 "${vmid}-${disk}>${target_vmid}-${target_disk}",
4210 } elsif ($storeid) {
4211 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4213 die "cannot move disk '$disk', only configured disks can be moved to another storage\n"
4214 if $disk =~ m/^unused\d+$/;
4216 $load_and_check_move->(); # early checks before forking/locking
4219 PVE
::QemuConfig-
>lock_config($vmid, $move_updatefn);
4222 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
4224 my $msg = "both 'storage' and 'target-vmid' missing, either needs to be set";
4225 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4229 my $check_vm_disks_local = sub {
4230 my ($storecfg, $vmconf, $vmid) = @_;
4232 my $local_disks = {};
4234 # add some more information to the disks e.g. cdrom
4235 PVE
::QemuServer
::foreach_volid
($vmconf, sub {
4236 my ($volid, $attr) = @_;
4238 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
4240 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
4241 return if $scfg->{shared
};
4243 # The shared attr here is just a special case where the vdisk
4244 # is marked as shared manually
4245 return if $attr->{shared
};
4246 return if $attr->{cdrom
} and $volid eq "none";
4248 if (exists $local_disks->{$volid}) {
4249 @{$local_disks->{$volid}}{keys %$attr} = values %$attr
4251 $local_disks->{$volid} = $attr;
4252 # ensure volid is present in case it's needed
4253 $local_disks->{$volid}->{volid
} = $volid;
4257 return $local_disks;
4260 __PACKAGE__-
>register_method({
4261 name
=> 'migrate_vm_precondition',
4262 path
=> '{vmid}/migrate',
4266 description
=> "Get preconditions for migration.",
4268 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4271 additionalProperties
=> 0,
4273 node
=> get_standard_option
('pve-node'),
4274 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4275 target
=> get_standard_option
('pve-node', {
4276 description
=> "Target node.",
4277 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4285 running
=> { type
=> 'boolean' },
4289 description
=> "List nodes allowed for offline migration, only passed if VM is offline"
4291 not_allowed_nodes
=> {
4294 description
=> "List not allowed nodes with additional informations, only passed if VM is offline"
4298 description
=> "List local disks including CD-Rom, unsused and not referenced disks"
4300 local_resources
=> {
4302 description
=> "List local resources e.g. pci, usb"
4304 'mapped-resources' => {
4306 description
=> "List of mapped resources e.g. pci, usb"
4313 my $rpcenv = PVE
::RPCEnvironment
::get
();
4315 my $authuser = $rpcenv->get_user();
4317 PVE
::Cluster
::check_cfs_quorum
();
4321 my $vmid = extract_param
($param, 'vmid');
4322 my $target = extract_param
($param, 'target');
4323 my $localnode = PVE
::INotify
::nodename
();
4327 my $vmconf = PVE
::QemuConfig-
>load_config($vmid);
4328 my $storecfg = PVE
::Storage
::config
();
4331 # try to detect errors early
4332 PVE
::QemuConfig-
>check_lock($vmconf);
4334 $res->{running
} = PVE
::QemuServer
::check_running
($vmid) ?
1:0;
4336 my ($local_resources, $mapped_resources, $missing_mappings_by_node) =
4337 PVE
::QemuServer
::check_local_resources
($vmconf, 1);
4338 delete $missing_mappings_by_node->{$localnode};
4340 # if vm is not running, return target nodes where local storage/mapped devices are available
4341 # for offline migration
4342 if (!$res->{running
}) {
4343 $res->{allowed_nodes
} = [];
4344 my $checked_nodes = PVE
::QemuServer
::check_local_storage_availability
($vmconf, $storecfg);
4345 delete $checked_nodes->{$localnode};
4347 foreach my $node (keys %$checked_nodes) {
4348 my $missing_mappings = $missing_mappings_by_node->{$node};
4349 if (scalar($missing_mappings->@*)) {
4350 $checked_nodes->{$node}->{'unavailable-resources'} = $missing_mappings;
4354 if (!defined($checked_nodes->{$node}->{unavailable_storages
})) {
4355 push @{$res->{allowed_nodes
}}, $node;
4359 $res->{not_allowed_nodes
} = $checked_nodes;
4362 my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
4363 $res->{local_disks
} = [ values %$local_disks ];;
4365 $res->{local_resources
} = $local_resources;
4366 $res->{'mapped-resources'} = $mapped_resources;
4373 __PACKAGE__-
>register_method({
4374 name
=> 'migrate_vm',
4375 path
=> '{vmid}/migrate',
4379 description
=> "Migrate virtual machine. Creates a new migration task.",
4381 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4384 additionalProperties
=> 0,
4386 node
=> get_standard_option
('pve-node'),
4387 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4388 target
=> get_standard_option
('pve-node', {
4389 description
=> "Target node.",
4390 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4394 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4399 description
=> "Allow to migrate VMs which use local devices. Only root may use this option.",
4404 enum
=> ['secure', 'insecure'],
4405 description
=> "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
4408 migration_network
=> {
4409 type
=> 'string', format
=> 'CIDR',
4410 description
=> "CIDR of the (sub) network that is used for migration.",
4413 "with-local-disks" => {
4415 description
=> "Enable live storage migration for local disk",
4418 targetstorage
=> get_standard_option
('pve-targetstorage', {
4419 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4422 description
=> "Override I/O bandwidth limit (in KiB/s).",
4426 default => 'migrate limit from datacenter or storage config',
4432 description
=> "the task ID.",
4437 my $rpcenv = PVE
::RPCEnvironment
::get
();
4438 my $authuser = $rpcenv->get_user();
4440 my $target = extract_param
($param, 'target');
4442 my $localnode = PVE
::INotify
::nodename
();
4443 raise_param_exc
({ target
=> "target is local node."}) if $target eq $localnode;
4445 PVE
::Cluster
::check_cfs_quorum
();
4447 PVE
::Cluster
::check_node_exists
($target);
4449 my $targetip = PVE
::Cluster
::remote_node_ip
($target);
4451 my $vmid = extract_param
($param, 'vmid');
4453 raise_param_exc
({ force
=> "Only root may use this option." })
4454 if $param->{force
} && $authuser ne 'root@pam';
4456 raise_param_exc
({ migration_type
=> "Only root may use this option." })
4457 if $param->{migration_type
} && $authuser ne 'root@pam';
4459 # allow root only until better network permissions are available
4460 raise_param_exc
({ migration_network
=> "Only root may use this option." })
4461 if $param->{migration_network
} && $authuser ne 'root@pam';
4464 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4466 # try to detect errors early
4468 PVE
::QemuConfig-
>check_lock($conf);
4470 if (PVE
::QemuServer
::check_running
($vmid)) {
4471 die "can't migrate running VM without --online\n" if !$param->{online
};
4473 my $repl_conf = PVE
::ReplicationConfig-
>new();
4474 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
4475 my $is_replicated_to_target = defined($repl_conf->find_local_replication_job($vmid, $target));
4476 if (!$param->{force
} && $is_replicated && !$is_replicated_to_target) {
4477 die "Cannot live-migrate replicated VM to node '$target' - not a replication " .
4478 "target. Use 'force' to override.\n";
4481 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4482 $param->{online
} = 0;
4485 my $storecfg = PVE
::Storage
::config
();
4486 if (my $targetstorage = $param->{targetstorage
}) {
4487 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($targetstorage, 'pve-storage-id') };
4488 raise_param_exc
({ targetstorage
=> "failed to parse storage map: $@" })
4491 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk'])
4492 if !defined($storagemap->{identity
});
4494 foreach my $target_sid (values %{$storagemap->{entries
}}) {
4495 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $target_sid, $target);
4498 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storagemap->{default}, $target)
4499 if $storagemap->{default};
4501 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target)
4502 if $storagemap->{identity
};
4504 $param->{storagemap
} = $storagemap;
4506 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target);
4509 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
4514 print "Requesting HA migration for VM $vmid to node $target\n";
4516 my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
4517 PVE
::Tools
::run_command
($cmd);
4521 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
4526 PVE
::QemuMigrate-
>migrate($target, $targetip, $vmid, $param);
4530 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
4533 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
4538 __PACKAGE__-
>register_method({
4539 name
=> 'remote_migrate_vm',
4540 path
=> '{vmid}/remote_migrate',
4544 description
=> "Migrate virtual machine to a remote cluster. Creates a new migration task. EXPERIMENTAL feature!",
4546 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4549 additionalProperties
=> 0,
4551 node
=> get_standard_option
('pve-node'),
4552 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4553 'target-vmid' => get_standard_option
('pve-vmid', { optional
=> 1 }),
4554 'target-endpoint' => get_standard_option
('proxmox-remote', {
4555 description
=> "Remote target endpoint",
4559 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4564 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.",
4568 'target-storage' => get_standard_option
('pve-targetstorage', {
4569 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4572 'target-bridge' => {
4574 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.",
4575 format
=> 'bridge-pair-list',
4578 description
=> "Override I/O bandwidth limit (in KiB/s).",
4582 default => 'migrate limit from datacenter or storage config',
4588 description
=> "the task ID.",
4593 my $rpcenv = PVE
::RPCEnvironment
::get
();
4594 my $authuser = $rpcenv->get_user();
4596 my $source_vmid = extract_param
($param, 'vmid');
4597 my $target_endpoint = extract_param
($param, 'target-endpoint');
4598 my $target_vmid = extract_param
($param, 'target-vmid') // $source_vmid;
4600 my $delete = extract_param
($param, 'delete') // 0;
4602 PVE
::Cluster
::check_cfs_quorum
();
4605 my $conf = PVE
::QemuConfig-
>load_config($source_vmid);
4607 PVE
::QemuConfig-
>check_lock($conf);
4609 raise_param_exc
({ vmid
=> "cannot migrate HA-managed VM to remote cluster" })
4610 if PVE
::HA
::Config
::vm_is_ha_managed
($source_vmid);
4612 my $remote = PVE
::JSONSchema
::parse_property_string
('proxmox-remote', $target_endpoint);
4614 # TODO: move this as helper somewhere appropriate?
4616 protocol
=> 'https',
4617 host
=> $remote->{host
},
4618 port
=> $remote->{port
} // 8006,
4619 apitoken
=> $remote->{apitoken
},
4623 if ($fp = $remote->{fingerprint
}) {
4624 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 };
4627 print "Establishing API connection with remote at '$remote->{host}'\n";
4629 my $api_client = PVE
::APIClient
::LWP-
>new(%$conn_args);
4631 if (!defined($fp)) {
4632 my $cert_info = $api_client->get("/nodes/localhost/certificates/info");
4633 foreach my $cert (@$cert_info) {
4634 my $filename = $cert->{filename
};
4635 next if $filename ne 'pveproxy-ssl.pem' && $filename ne 'pve-ssl.pem';
4636 $fp = $cert->{fingerprint
} if !$fp || $filename eq 'pveproxy-ssl.pem';
4638 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 }
4642 my $repl_conf = PVE
::ReplicationConfig-
>new();
4643 my $is_replicated = $repl_conf->check_for_existing_jobs($source_vmid, 1);
4644 die "cannot remote-migrate replicated VM\n" if $is_replicated;
4646 if (PVE
::QemuServer
::check_running
($source_vmid)) {
4647 die "can't migrate running VM without --online\n" if !$param->{online
};
4650 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4651 $param->{online
} = 0;
4654 my $storecfg = PVE
::Storage
::config
();
4655 my $target_storage = extract_param
($param, 'target-storage');
4656 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($target_storage, 'pve-storage-id') };
4657 raise_param_exc
({ 'target-storage' => "failed to parse storage map: $@" })
4660 my $target_bridge = extract_param
($param, 'target-bridge');
4661 my $bridgemap = eval { PVE
::JSONSchema
::parse_idmap
($target_bridge, 'pve-bridge-id') };
4662 raise_param_exc
({ 'target-bridge' => "failed to parse bridge map: $@" })
4665 die "remote migration requires explicit storage mapping!\n"
4666 if $storagemap->{identity
};
4668 $param->{storagemap
} = $storagemap;
4669 $param->{bridgemap
} = $bridgemap;
4670 $param->{remote
} = {
4671 conn
=> $conn_args, # re-use fingerprint for tunnel
4672 client
=> $api_client,
4673 vmid
=> $target_vmid,
4675 $param->{migration_type
} = 'websocket';
4676 $param->{'with-local-disks'} = 1;
4677 $param->{delete} = $delete if $delete;
4679 my $cluster_status = $api_client->get("/cluster/status");
4681 foreach my $entry (@$cluster_status) {
4682 next if $entry->{type
} ne 'node';
4683 if ($entry->{local}) {
4684 $target_node = $entry->{name
};
4689 die "couldn't determine endpoint's node name\n"
4690 if !defined($target_node);
4693 PVE
::QemuMigrate-
>migrate($target_node, $remote->{host
}, $source_vmid, $param);
4697 return PVE
::GuestHelpers
::guest_migration_lock
($source_vmid, 10, $realcmd);
4700 return $rpcenv->fork_worker('qmigrate', $source_vmid, $authuser, $worker);
4703 __PACKAGE__-
>register_method({
4705 path
=> '{vmid}/monitor',
4709 description
=> "Execute QEMU monitor commands.",
4711 description
=> "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
4712 check
=> ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
4715 additionalProperties
=> 0,
4717 node
=> get_standard_option
('pve-node'),
4718 vmid
=> get_standard_option
('pve-vmid'),
4721 description
=> "The monitor command.",
4725 returns
=> { type
=> 'string'},
4729 my $rpcenv = PVE
::RPCEnvironment
::get
();
4730 my $authuser = $rpcenv->get_user();
4733 my $command = shift;
4734 return $command =~ m/^\s*info(\s+|$)/
4735 || $command =~ m/^\s*help\s*$/;
4738 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
4739 if !&$is_ro($param->{command
});
4741 my $vmid = $param->{vmid
};
4743 my $conf = PVE
::QemuConfig-
>load_config ($vmid); # check if VM exists
4747 $res = PVE
::QemuServer
::Monitor
::hmp_cmd
($vmid, $param->{command
});
4749 $res = "ERROR: $@" if $@;
4754 __PACKAGE__-
>register_method({
4755 name
=> 'resize_vm',
4756 path
=> '{vmid}/resize',
4760 description
=> "Extend volume size.",
4762 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
4765 additionalProperties
=> 0,
4767 node
=> get_standard_option
('pve-node'),
4768 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4769 skiplock
=> get_standard_option
('skiplock'),
4772 description
=> "The disk you want to resize.",
4773 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
4777 pattern
=> '\+?\d+(\.\d+)?[KMGT]?',
4778 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.",
4782 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
4790 description
=> "the task ID.",
4795 my $rpcenv = PVE
::RPCEnvironment
::get
();
4797 my $authuser = $rpcenv->get_user();
4799 my $node = extract_param
($param, 'node');
4801 my $vmid = extract_param
($param, 'vmid');
4803 my $digest = extract_param
($param, 'digest');
4805 my $disk = extract_param
($param, 'disk');
4807 my $sizestr = extract_param
($param, 'size');
4809 my $skiplock = extract_param
($param, 'skiplock');
4810 raise_param_exc
({ skiplock
=> "Only root may use this option." })
4811 if $skiplock && $authuser ne 'root@pam';
4813 my $storecfg = PVE
::Storage
::config
();
4815 my $updatefn = sub {
4817 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4819 die "checksum missmatch (file change by other user?)\n"
4820 if $digest && $digest ne $conf->{digest
};
4821 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
4823 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4825 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
4827 my (undef, undef, undef, undef, undef, undef, $format) =
4828 PVE
::Storage
::parse_volname
($storecfg, $drive->{file
});
4830 my $volid = $drive->{file
};
4832 die "disk '$disk' has no associated volume\n" if !$volid;
4834 die "you can't resize a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive);
4836 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
4838 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4840 PVE
::Storage
::activate_volumes
($storecfg, [$volid]);
4841 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid, 5);
4843 die "Could not determine current size of volume '$volid'\n" if !defined($size);
4845 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
4846 my ($ext, $newsize, $unit) = ($1, $2, $4);
4849 $newsize = $newsize * 1024;
4850 } elsif ($unit eq 'M') {
4851 $newsize = $newsize * 1024 * 1024;
4852 } elsif ($unit eq 'G') {
4853 $newsize = $newsize * 1024 * 1024 * 1024;
4854 } elsif ($unit eq 'T') {
4855 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
4858 $newsize += $size if $ext;
4859 $newsize = int($newsize);
4861 die "shrinking disks is not supported\n" if $newsize < $size;
4863 return if $size == $newsize;
4865 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
4867 PVE
::QemuServer
::qemu_block_resize
($vmid, "drive-$disk", $storecfg, $volid, $newsize);
4869 $drive->{size
} = $newsize;
4870 $conf->{$disk} = PVE
::QemuServer
::print_drive
($drive);
4872 PVE
::QemuConfig-
>write_config($vmid, $conf);
4876 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
4879 return $rpcenv->fork_worker('resize', $vmid, $authuser, $worker);
4882 __PACKAGE__-
>register_method({
4883 name
=> 'snapshot_list',
4884 path
=> '{vmid}/snapshot',
4886 description
=> "List all snapshots.",
4888 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
4891 protected
=> 1, # qemu pid files are only readable by root
4893 additionalProperties
=> 0,
4895 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4896 node
=> get_standard_option
('pve-node'),
4905 description
=> "Snapshot identifier. Value 'current' identifies the current VM.",
4909 description
=> "Snapshot includes RAM.",
4914 description
=> "Snapshot description.",
4918 description
=> "Snapshot creation time",
4920 renderer
=> 'timestamp',
4924 description
=> "Parent snapshot identifier.",
4930 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
4935 my $vmid = $param->{vmid
};
4937 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4938 my $snaphash = $conf->{snapshots
} || {};
4942 foreach my $name (keys %$snaphash) {
4943 my $d = $snaphash->{$name};
4946 snaptime
=> $d->{snaptime
} || 0,
4947 vmstate
=> $d->{vmstate
} ?
1 : 0,
4948 description
=> $d->{description
} || '',
4950 $item->{parent
} = $d->{parent
} if $d->{parent
};
4951 $item->{snapstate
} = $d->{snapstate
} if $d->{snapstate
};
4955 my $running = PVE
::QemuServer
::check_running
($vmid, 1) ?
1 : 0;
4958 digest
=> $conf->{digest
},
4959 running
=> $running,
4960 description
=> "You are here!",
4962 $current->{parent
} = $conf->{parent
} if $conf->{parent
};
4964 push @$res, $current;
4969 __PACKAGE__-
>register_method({
4971 path
=> '{vmid}/snapshot',
4975 description
=> "Snapshot a VM.",
4977 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
4980 additionalProperties
=> 0,
4982 node
=> get_standard_option
('pve-node'),
4983 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4984 snapname
=> get_standard_option
('pve-snapshot-name'),
4988 description
=> "Save the vmstate",
4993 description
=> "A textual description or comment.",
4999 description
=> "the task ID.",
5004 my $rpcenv = PVE
::RPCEnvironment
::get
();
5006 my $authuser = $rpcenv->get_user();
5008 my $node = extract_param
($param, 'node');
5010 my $vmid = extract_param
($param, 'vmid');
5012 my $snapname = extract_param
($param, 'snapname');
5014 die "unable to use snapshot name 'current' (reserved name)\n"
5015 if $snapname eq 'current';
5017 die "unable to use snapshot name 'pending' (reserved name)\n"
5018 if lc($snapname) eq 'pending';
5021 PVE
::Cluster
::log_msg
('info', $authuser, "snapshot VM $vmid: $snapname");
5022 PVE
::QemuConfig-
>snapshot_create($vmid, $snapname, $param->{vmstate
},
5023 $param->{description
});
5026 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
5029 __PACKAGE__-
>register_method({
5030 name
=> 'snapshot_cmd_idx',
5031 path
=> '{vmid}/snapshot/{snapname}',
5038 additionalProperties
=> 0,
5040 vmid
=> get_standard_option
('pve-vmid'),
5041 node
=> get_standard_option
('pve-node'),
5042 snapname
=> get_standard_option
('pve-snapshot-name'),
5051 links
=> [ { rel
=> 'child', href
=> "{cmd}" } ],
5058 push @$res, { cmd
=> 'rollback' };
5059 push @$res, { cmd
=> 'config' };
5064 __PACKAGE__-
>register_method({
5065 name
=> 'update_snapshot_config',
5066 path
=> '{vmid}/snapshot/{snapname}/config',
5070 description
=> "Update snapshot metadata.",
5072 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5075 additionalProperties
=> 0,
5077 node
=> get_standard_option
('pve-node'),
5078 vmid
=> get_standard_option
('pve-vmid'),
5079 snapname
=> get_standard_option
('pve-snapshot-name'),
5083 description
=> "A textual description or comment.",
5087 returns
=> { type
=> 'null' },
5091 my $rpcenv = PVE
::RPCEnvironment
::get
();
5093 my $authuser = $rpcenv->get_user();
5095 my $vmid = extract_param
($param, 'vmid');
5097 my $snapname = extract_param
($param, 'snapname');
5099 return if !defined($param->{description
});
5101 my $updatefn = sub {
5103 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5105 PVE
::QemuConfig-
>check_lock($conf);
5107 my $snap = $conf->{snapshots
}->{$snapname};
5109 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5111 $snap->{description
} = $param->{description
} if defined($param->{description
});
5113 PVE
::QemuConfig-
>write_config($vmid, $conf);
5116 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
5121 __PACKAGE__-
>register_method({
5122 name
=> 'get_snapshot_config',
5123 path
=> '{vmid}/snapshot/{snapname}/config',
5126 description
=> "Get snapshot configuration",
5128 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback', 'VM.Audit' ], any
=> 1],
5131 additionalProperties
=> 0,
5133 node
=> get_standard_option
('pve-node'),
5134 vmid
=> get_standard_option
('pve-vmid'),
5135 snapname
=> get_standard_option
('pve-snapshot-name'),
5138 returns
=> { type
=> "object" },
5142 my $rpcenv = PVE
::RPCEnvironment
::get
();
5144 my $authuser = $rpcenv->get_user();
5146 my $vmid = extract_param
($param, 'vmid');
5148 my $snapname = extract_param
($param, 'snapname');
5150 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5152 my $snap = $conf->{snapshots
}->{$snapname};
5154 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5159 __PACKAGE__-
>register_method({
5161 path
=> '{vmid}/snapshot/{snapname}/rollback',
5165 description
=> "Rollback VM state to specified snapshot.",
5167 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any
=> 1],
5170 additionalProperties
=> 0,
5172 node
=> get_standard_option
('pve-node'),
5173 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5174 snapname
=> get_standard_option
('pve-snapshot-name'),
5177 description
=> "Whether the VM should get started after rolling back successfully."
5178 . " (Note: VMs will be automatically started if the snapshot includes RAM.)",
5186 description
=> "the task ID.",
5191 my $rpcenv = PVE
::RPCEnvironment
::get
();
5193 my $authuser = $rpcenv->get_user();
5195 my $node = extract_param
($param, 'node');
5197 my $vmid = extract_param
($param, 'vmid');
5199 my $snapname = extract_param
($param, 'snapname');
5202 PVE
::Cluster
::log_msg
('info', $authuser, "rollback snapshot VM $vmid: $snapname");
5203 PVE
::QemuConfig-
>snapshot_rollback($vmid, $snapname);
5205 if ($param->{start
} && !PVE
::QemuServer
::Helpers
::vm_running_locally
($vmid)) {
5206 PVE
::API2
::Qemu-
>vm_start({ vmid
=> $vmid, node
=> $node });
5211 # hold migration lock, this makes sure that nobody create replication snapshots
5212 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
5215 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
5218 __PACKAGE__-
>register_method({
5219 name
=> 'delsnapshot',
5220 path
=> '{vmid}/snapshot/{snapname}',
5224 description
=> "Delete a VM snapshot.",
5226 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5229 additionalProperties
=> 0,
5231 node
=> get_standard_option
('pve-node'),
5232 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5233 snapname
=> get_standard_option
('pve-snapshot-name'),
5237 description
=> "For removal from config file, even if removing disk snapshots fails.",
5243 description
=> "the task ID.",
5248 my $rpcenv = PVE
::RPCEnvironment
::get
();
5250 my $authuser = $rpcenv->get_user();
5252 my $node = extract_param
($param, 'node');
5254 my $vmid = extract_param
($param, 'vmid');
5256 my $snapname = extract_param
($param, 'snapname');
5259 my $do_delete = sub {
5261 PVE
::Cluster
::log_msg
('info', $authuser, "delete snapshot VM $vmid: $snapname");
5262 PVE
::QemuConfig-
>snapshot_delete($vmid, $snapname, $param->{force
});
5266 if ($param->{force
}) {
5269 eval { PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $do_delete); };
5271 die $err if $lock_obtained;
5272 die "Failed to obtain guest migration lock - replication running?\n";
5277 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
5280 __PACKAGE__-
>register_method({
5282 path
=> '{vmid}/template',
5286 description
=> "Create a Template.",
5288 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid}",
5289 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
5292 additionalProperties
=> 0,
5294 node
=> get_standard_option
('pve-node'),
5295 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
5299 description
=> "If you want to convert only 1 disk to base image.",
5300 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
5307 description
=> "the task ID.",
5312 my $rpcenv = PVE
::RPCEnvironment
::get
();
5314 my $authuser = $rpcenv->get_user();
5316 my $node = extract_param
($param, 'node');
5318 my $vmid = extract_param
($param, 'vmid');
5320 my $disk = extract_param
($param, 'disk');
5322 my $load_and_check = sub {
5323 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5325 PVE
::QemuConfig-
>check_lock($conf);
5327 die "unable to create template, because VM contains snapshots\n"
5328 if $conf->{snapshots
} && scalar(keys %{$conf->{snapshots
}});
5330 die "you can't convert a template to a template\n"
5331 if PVE
::QemuConfig-
>is_template($conf) && !$disk;
5333 die "you can't convert a VM to template if VM is running\n"
5334 if PVE
::QemuServer
::check_running
($vmid);
5339 $load_and_check->();
5342 PVE
::QemuConfig-
>lock_config($vmid, sub {
5343 my $conf = $load_and_check->();
5345 $conf->{template
} = 1;
5346 PVE
::QemuConfig-
>write_config($vmid, $conf);
5348 PVE
::QemuServer
::template_create
($vmid, $conf, $disk);
5352 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
5355 __PACKAGE__-
>register_method({
5356 name
=> 'cloudinit_generated_config_dump',
5357 path
=> '{vmid}/cloudinit/dump',
5360 description
=> "Get automatically generated cloudinit config.",
5362 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5365 additionalProperties
=> 0,
5367 node
=> get_standard_option
('pve-node'),
5368 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5370 description
=> 'Config type.',
5372 enum
=> ['user', 'network', 'meta'],
5382 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
5384 return PVE
::QemuServer
::Cloudinit
::dump_cloudinit_config
($conf, $param->{vmid
}, $param->{type
});
5387 __PACKAGE__-
>register_method({
5389 path
=> '{vmid}/mtunnel',
5392 description
=> 'Migration tunnel endpoint - only for internal use by VM migration.',
5396 ['perm', '/vms/{vmid}', [ 'VM.Allocate' ]],
5397 ['perm', '/', [ 'Sys.Incoming' ]],
5399 description
=> "You need 'VM.Allocate' permissions on '/vms/{vmid}' and Sys.Incoming" .
5400 " on '/'. Further permission checks happen during the actual migration.",
5403 additionalProperties
=> 0,
5405 node
=> get_standard_option
('pve-node'),
5406 vmid
=> get_standard_option
('pve-vmid'),
5409 format
=> 'pve-storage-id-list',
5411 description
=> 'List of storages to check permission and availability. Will be checked again for all actually used storages during migration.',
5415 format
=> 'pve-bridge-id-list',
5417 description
=> 'List of network bridges to check availability. Will be checked again for actually used bridges during migration.',
5422 additionalProperties
=> 0,
5424 upid
=> { type
=> 'string' },
5425 ticket
=> { type
=> 'string' },
5426 socket => { type
=> 'string' },
5432 my $rpcenv = PVE
::RPCEnvironment
::get
();
5433 my $authuser = $rpcenv->get_user();
5435 my $node = extract_param
($param, 'node');
5436 my $vmid = extract_param
($param, 'vmid');
5438 my $storages = extract_param
($param, 'storages');
5439 my $bridges = extract_param
($param, 'bridges');
5441 my $nodename = PVE
::INotify
::nodename
();
5443 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
5444 if $node ne 'localhost' && $node ne $nodename;
5448 my $storecfg = PVE
::Storage
::config
();
5449 foreach my $storeid (PVE
::Tools
::split_list
($storages)) {
5450 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storeid, $node);
5453 foreach my $bridge (PVE
::Tools
::split_list
($bridges)) {
5454 PVE
::Network
::read_bridge_mtu
($bridge);
5457 PVE
::Cluster
::check_cfs_quorum
();
5459 my $lock = 'create';
5460 eval { PVE
::QemuConfig-
>create_and_lock_config($vmid, 0, $lock); };
5462 raise_param_exc
({ vmid
=> "unable to create empty VM config - $@"})
5467 storecfg
=> PVE
::Storage
::config
(),
5472 my $run_locked = sub {
5473 my ($code, $params) = @_;
5474 return PVE
::QemuConfig-
>lock_config($state->{vmid
}, sub {
5475 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5477 $state->{conf
} = $conf;
5479 die "Encountered wrong lock - aborting mtunnel command handling.\n"
5480 if $state->{lock} && !PVE
::QemuConfig-
>has_lock($conf, $state->{lock});
5482 return $code->($params);
5490 description
=> 'Full VM config, adapted for target cluster/node',
5492 'firewall-config' => {
5494 description
=> 'VM firewall config',
5499 format
=> PVE
::JSONSchema
::get_standard_option
('pve-qm-image-format'),
5502 format
=> 'pve-storage-id',
5506 description
=> 'parsed drive information without volid and format',
5512 description
=> 'params passed to vm_start_nolock',
5516 description
=> 'migrate_opts passed to vm_start_nolock',
5522 description
=> 'socket path for which the ticket should be valid. must be known to current mtunnel instance.',
5528 description
=> 'remove VM config and disks, aborting migration',
5532 'disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'disk-import'},
5533 'query-disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'query-disk-import'},
5534 bwlimit
=> $PVE::StorageTunnel
::cmd_schema-
>{bwlimit
},
5537 my $cmd_handlers = {
5539 # compared against other end's version
5540 # bump/reset for breaking changes
5541 # bump/bump for opt-in changes
5543 api
=> $PVE::QemuMigrate
::WS_TUNNEL_VERSION
,
5550 # parse and write out VM FW config if given
5551 if (my $fw_conf = $params->{'firewall-config'}) {
5552 my ($path, $fh) = PVE
::Tools
::tempfile_contents
($fw_conf, 700);
5559 ipset_comments
=> {},
5561 my $cluster_fw_conf = PVE
::Firewall
::load_clusterfw_conf
();
5563 # TODO: add flag for strict parsing?
5564 # TODO: add import sub that does all this given raw content?
5565 my $vmfw_conf = PVE
::Firewall
::generic_fw_config_parser
($path, $cluster_fw_conf, $empty_conf, 'vm');
5566 $vmfw_conf->{vmid
} = $state->{vmid
};
5567 PVE
::Firewall
::save_vmfw_conf
($state->{vmid
}, $vmfw_conf);
5569 $state->{cleanup
}->{fw
} = 1;
5572 my $conf_fn = "incoming/qemu-server/$state->{vmid}.conf";
5573 my $new_conf = PVE
::QemuServer
::parse_vm_config
($conf_fn, $params->{conf
}, 1);
5574 delete $new_conf->{lock};
5575 delete $new_conf->{digest
};
5577 # TODO handle properly?
5578 delete $new_conf->{snapshots
};
5579 delete $new_conf->{parent
};
5580 delete $new_conf->{pending
};
5582 # not handled by update_vm_api
5583 my $vmgenid = delete $new_conf->{vmgenid
};
5584 my $meta = delete $new_conf->{meta
};
5585 my $cloudinit = delete $new_conf->{cloudinit
}; # this is informational only
5586 $new_conf->{skip_cloud_init
} = 1; # re-use image from source side
5588 $new_conf->{vmid
} = $state->{vmid
};
5589 $new_conf->{node
} = $node;
5591 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, 'create');
5594 $update_vm_api->($new_conf, 1);
5597 # revert to locked previous config
5598 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5599 $conf->{lock} = 'create';
5600 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5605 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5606 $conf->{lock} = 'migrate';
5607 $conf->{vmgenid
} = $vmgenid if defined($vmgenid);
5608 $conf->{meta
} = $meta if defined($meta);
5609 $conf->{cloudinit
} = $cloudinit if defined($cloudinit);
5610 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5612 $state->{lock} = 'migrate';
5618 return PVE
::StorageTunnel
::handle_bwlimit
($params);
5623 my $format = $params->{format
};
5624 my $storeid = $params->{storage
};
5625 my $drive = $params->{drive
};
5627 $check_storage_access_migrate->($rpcenv, $authuser, $state->{storecfg
}, $storeid, $node);
5630 default => $storeid,
5633 my $source_volumes = {
5644 my $res = PVE
::QemuServer
::vm_migrate_alloc_nbd_disks
($state->{storecfg
}, $state->{vmid
}, $source_volumes, $storagemap);
5645 if (defined($res->{disk
})) {
5646 $state->{cleanup
}->{volumes
}->{$res->{disk
}->{volid
}} = 1;
5647 return $res->{disk
};
5649 die "failed to allocate NBD disk..\n";
5652 'disk-import' => sub {
5655 $check_storage_access_migrate->(
5663 $params->{unix
} = "/run/qemu-server/$state->{vmid}.storage";
5665 return PVE
::StorageTunnel
::handle_disk_import
($state, $params);
5667 'query-disk-import' => sub {
5670 return PVE
::StorageTunnel
::handle_query_disk_import
($state, $params);
5675 my $info = PVE
::QemuServer
::vm_start_nolock
(
5679 $params->{start_params
},
5680 $params->{migrate_opts
},
5684 if ($info->{migrate
}->{proto
} ne 'unix') {
5685 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5686 die "migration over non-UNIX sockets not possible\n";
5689 my $socket = $info->{migrate
}->{addr
};
5690 chown $state->{socket_uid
}, -1, $socket;
5691 $state->{sockets
}->{$socket} = 1;
5693 my $unix_sockets = $info->{migrate
}->{unix_sockets
};
5694 foreach my $socket (@$unix_sockets) {
5695 chown $state->{socket_uid
}, -1, $socket;
5696 $state->{sockets
}->{$socket} = 1;
5701 if (PVE
::QemuServer
::qga_check_running
($state->{vmid
})) {
5702 eval { mon_cmd
($state->{vmid
}, "guest-fstrim") };
5703 warn "fstrim failed: $@\n" if $@;
5708 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5712 PVE
::QemuServer
::nbd_stop
($state->{vmid
});
5716 if (PVE
::QemuServer
::Helpers
::vm_running_locally
($state->{vmid
})) {
5717 PVE
::QemuServer
::vm_resume
($state->{vmid
}, 1, 1);
5719 die "VM $state->{vmid} not running\n";
5724 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, $state->{lock});
5725 delete $state->{lock};
5731 my $path = $params->{path
};
5733 die "Not allowed to generate ticket for unknown socket '$path'\n"
5734 if !defined($state->{sockets
}->{$path});
5736 return { ticket
=> PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$path") };
5741 if ($params->{cleanup
}) {
5742 if ($state->{cleanup
}->{fw
}) {
5743 PVE
::Firewall
::remove_vmfw_conf
($state->{vmid
});
5746 for my $volid (keys $state->{cleanup
}->{volumes
}->%*) {
5747 print "freeing volume '$volid' as part of cleanup\n";
5748 eval { PVE
::Storage
::vdisk_free
($state->{storecfg
}, $volid) };
5752 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5755 print "switching to exit-mode, waiting for client to disconnect\n";
5762 my $socket_addr = "/run/qemu-server/$state->{vmid}.mtunnel";
5763 unlink $socket_addr;
5765 $state->{socket} = IO
::Socket
::UNIX-
>new(
5766 Type
=> SOCK_STREAM
(),
5767 Local
=> $socket_addr,
5771 $state->{socket_uid
} = getpwnam('www-data')
5772 or die "Failed to resolve user 'www-data' to numeric UID\n";
5773 chown $state->{socket_uid
}, -1, $socket_addr;
5776 print "mtunnel started\n";
5778 my $conn = eval { PVE
::Tools
::run_with_timeout
(300, sub { $state->{socket}->accept() }) };
5780 warn "Failed to accept tunnel connection - $@\n";
5782 warn "Removing tunnel socket..\n";
5783 unlink $state->{socket};
5785 warn "Removing temporary VM config..\n";
5787 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5790 die "Exiting mtunnel\n";
5793 $state->{conn
} = $conn;
5795 my $reply_err = sub {
5798 my $reply = JSON
::encode_json
({
5799 success
=> JSON
::false
,
5802 $conn->print("$reply\n");
5806 my $reply_ok = sub {
5809 $res->{success
} = JSON
::true
;
5810 my $reply = JSON
::encode_json
($res);
5811 $conn->print("$reply\n");
5815 while (my $line = <$conn>) {
5818 # untaint, we validate below if needed
5819 ($line) = $line =~ /^(.*)$/;
5820 my $parsed = eval { JSON
::decode_json
($line) };
5822 $reply_err->("failed to parse command - $@");
5826 my $cmd = delete $parsed->{cmd
};
5827 if (!defined($cmd)) {
5828 $reply_err->("'cmd' missing");
5829 } elsif ($state->{exit}) {
5830 $reply_err->("tunnel is in exit-mode, processing '$cmd' cmd not possible");
5832 } elsif (my $handler = $cmd_handlers->{$cmd}) {
5833 print "received command '$cmd'\n";
5835 if ($cmd_desc->{$cmd}) {
5836 PVE
::JSONSchema
::validate
($parsed, $cmd_desc->{$cmd});
5840 my $res = $run_locked->($handler, $parsed);
5843 $reply_err->("failed to handle '$cmd' command - $@")
5846 $reply_err->("unknown command '$cmd' given");
5850 if ($state->{exit}) {
5851 print "mtunnel exited\n";
5853 die "mtunnel exited unexpectedly\n";
5857 my $socket_addr = "/run/qemu-server/$vmid.mtunnel";
5858 my $ticket = PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$socket_addr");
5859 my $upid = $rpcenv->fork_worker('qmtunnel', $vmid, $authuser, $realcmd);
5864 socket => $socket_addr,
5868 __PACKAGE__-
>register_method({
5869 name
=> 'mtunnelwebsocket',
5870 path
=> '{vmid}/mtunnelwebsocket',
5873 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.",
5874 user
=> 'all', # check inside
5876 description
=> 'Migration tunnel endpoint for websocket upgrade - only for internal use by VM migration.',
5878 additionalProperties
=> 0,
5880 node
=> get_standard_option
('pve-node'),
5881 vmid
=> get_standard_option
('pve-vmid'),
5884 description
=> "unix socket to forward to",
5888 description
=> "ticket return by initial 'mtunnel' API call, or retrieved via 'ticket' tunnel command",
5895 port
=> { type
=> 'string', optional
=> 1 },
5896 socket => { type
=> 'string', optional
=> 1 },
5902 my $rpcenv = PVE
::RPCEnvironment
::get
();
5903 my $authuser = $rpcenv->get_user();
5905 my $nodename = PVE
::INotify
::nodename
();
5906 my $node = extract_param
($param, 'node');
5908 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
5909 if $node ne 'localhost' && $node ne $nodename;
5911 my $vmid = $param->{vmid
};
5913 PVE
::QemuConfig-
>load_config($vmid);
5915 my $socket = $param->{socket};
5916 PVE
::AccessControl
::verify_tunnel_ticket
($param->{ticket
}, $authuser, "/socket/$socket");
5918 return { socket => $socket };