use PVE::QemuServer::ImportDisk;
use PVE::QemuServer::Monitor qw(mon_cmd);
use PVE::QemuServer::Machine;
+use PVE::QemuServer::Memory qw(get_current_memory);
+use PVE::QemuServer::PCI;
+use PVE::QemuServer::USB;
use PVE::QemuMigrate;
use PVE::RPCEnvironment;
use PVE::AccessControl;
use PVE::SSHInfo;
use PVE::Replication;
use PVE::StorageTunnel;
+use PVE::RESTEnvironment qw(log_warn);
BEGIN {
if (!$ENV{PVE_GENERATING_DOCS}) {
}
};
-my $NEW_DISK_RE = qr!^(([^/:\s]+):)?(\d+(\.\d+)?)$!;
-
my $check_drive_param = sub {
my ($param, $storecfg, $extra_checks) = @_;
raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
if ($drive->{'import-from'}) {
- if ($drive->{file} !~ $NEW_DISK_RE || $3 != 0) {
+ if ($drive->{file} !~ $PVE::QemuServer::Drive::NEW_DISK_RE || $3 != 0) {
raise_param_exc({
$opt => "'import-from' requires special syntax - ".
"use <storage ID>:0,import-from=<source>",
# nothing to check
} elsif ($isCDROM && ($volid eq 'cdrom')) {
$rpcenv->check($authuser, "/", ['Sys.Console']);
- } elsif (!$isCDROM && ($volid =~ $NEW_DISK_RE)) {
+ } elsif (!$isCDROM && ($volid =~ $PVE::QemuServer::Drive::NEW_DISK_RE)) {
my ($storeid, $size) = ($2 || $default_storage, $3);
die "no storage ID specified (and no default storage)\n" if !$storeid;
$rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
# Note: $pool is only needed when creating a VM, because pool permissions
# are automatically inherited if VM already exists inside a pool.
-my $create_disks = sub {
- my ($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $settings, $default_storage) = @_;
+my sub create_disks : prototype($$$$$$$$$$) {
+ my (
+ $rpcenv,
+ $authuser,
+ $conf,
+ $arch,
+ $storecfg,
+ $vmid,
+ $pool,
+ $settings,
+ $default_storage,
+ $is_live_import,
+ ) = @_;
my $vollist = [];
my $res = {};
+ my $live_import_mapping = {};
+
my $code = sub {
my ($ds, $disk) = @_;
delete $disk->{format}; # no longer needed
$res->{$ds} = PVE::QemuServer::print_drive($disk);
print "$ds: successfully created disk '$res->{$ds}'\n";
- } elsif ($volid =~ $NEW_DISK_RE) {
+ } elsif ($volid =~ $PVE::QemuServer::Drive::NEW_DISK_RE) {
my ($storeid, $size) = ($2 || $default_storage, $3);
die "no storage ID specified (and no default storage)\n" if !$storeid;
+ $size = PVE::Tools::convert_size($size, 'gb' => 'kb'); # vdisk_alloc uses kb
+
+ my $live_import = $is_live_import && $ds ne 'efidisk0';
+ my $needs_creation = 1;
+
if (my $source = delete $disk->{'import-from'}) {
my $dst_volid;
+ $needs_creation = $live_import;
+
if (PVE::Storage::parse_volume_id($source, 1)) { # PVE-managed volume
- my $dest_info = {
- vmid => $vmid,
- drivename => $ds,
- storage => $storeid,
- format => $disk->{format},
- };
+ if ($live_import && $ds ne 'efidisk0') {
+ my $path = PVE::Storage::path($storecfg, $source)
+ or die "failed to get a path for '$source'\n";
+ $source = $path;
+ ($size, my $source_format) = PVE::Storage::file_size_info($source);
+ die "could not get file size of $source\n" if !$size;
+ $live_import_mapping->{$ds} = {
+ path => $source,
+ format => $source_format,
+ };
+ } else {
+ my $dest_info = {
+ vmid => $vmid,
+ drivename => $ds,
+ storage => $storeid,
+ format => $disk->{format},
+ };
- $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($conf, $disk)
- if $ds eq 'efidisk0';
+ $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($conf, $disk)
+ if $ds eq 'efidisk0';
- ($dst_volid, $size) = eval {
- $import_from_volid->($storecfg, $source, $dest_info, $vollist);
- };
- die "cannot import from '$source' - $@" if $@;
+ ($dst_volid, $size) = eval {
+ $import_from_volid->($storecfg, $source, $dest_info, $vollist);
+ };
+ die "cannot import from '$source' - $@" if $@;
+ }
} else {
$source = PVE::Storage::abs_filesystem_path($storecfg, $source, 1);
- $size = PVE::Storage::file_size_info($source);
+ ($size, my $source_format) = PVE::Storage::file_size_info($source);
die "could not get file size of $source\n" if !$size;
- (undef, $dst_volid) = PVE::QemuServer::ImportDisk::do_import(
- $source,
- $vmid,
- $storeid,
- {
- drive_name => $ds,
- format => $disk->{format},
- 'skip-config-update' => 1,
- },
- );
- push @$vollist, $dst_volid;
+ if ($live_import && $ds ne 'efidisk0') {
+ $live_import_mapping->{$ds} = {
+ path => $source,
+ format => $source_format,
+ };
+ } else {
+ (undef, $dst_volid) = PVE::QemuServer::ImportDisk::do_import(
+ $source,
+ $vmid,
+ $storeid,
+ {
+ drive_name => $ds,
+ format => $disk->{format},
+ 'skip-config-update' => 1,
+ },
+ );
+ push @$vollist, $dst_volid;
+ }
}
- $disk->{file} = $dst_volid;
- $disk->{size} = $size;
- delete $disk->{format}; # no longer needed
- $res->{$ds} = PVE::QemuServer::print_drive($disk);
- } else {
+ if ($needs_creation) {
+ $size = PVE::Tools::convert_size($size, 'b' => 'kb'); # vdisk_alloc uses kb
+ } else {
+ $disk->{file} = $dst_volid;
+ $disk->{size} = $size;
+ delete $disk->{format}; # no longer needed
+ $res->{$ds} = PVE::QemuServer::print_drive($disk);
+ }
+ }
+
+ if ($needs_creation) {
my $defformat = PVE::Storage::storage_default_format($storecfg, $storeid);
my $fmt = $disk->{format} || $defformat;
- $size = PVE::Tools::convert_size($size, 'gb' => 'kb'); # vdisk_alloc uses kb
-
my $volid;
if ($ds eq 'efidisk0') {
my $smm = PVE::QemuServer::Machine::machine_type_is_q35($conf);
die $err;
}
- return ($vollist, $res);
+ # don't return empty import mappings
+ $live_import_mapping = undef if !%$live_import_mapping;
+
+ return ($vollist, $res, $live_import_mapping);
};
my $check_cpu_model_access = sub {
cipassword => 1,
citype => 1,
ciuser => 1,
+ ciupgrade => 1,
nameserver => 1,
searchdomain => 1,
sshkeys => 1,
return 1;
};
-my $check_vm_create_usb_perm = sub {
+my sub check_usb_perm {
+ my ($rpcenv, $authuser, $vmid, $pool, $opt, $value) = @_;
+
+ return 1 if $authuser eq 'root@pam';
+
+ $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
+
+ my $device = PVE::JSONSchema::parse_property_string('pve-qm-usb', $value);
+ if ($device->{host} && $device->{host} !~ m/^spice$/i) {
+ die "only root can set '$opt' config for real devices\n";
+ } elsif ($device->{mapping}) {
+ $rpcenv->check_full($authuser, "/mapping/usb/$device->{mapping}", ['Mapping.Use']);
+ } else {
+ die "either 'host' or 'mapping' must be set.\n";
+ }
+
+ return 1;
+}
+
+my sub check_vm_create_usb_perm {
my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
return 1 if $authuser eq 'root@pam';
foreach my $opt (keys %{$param}) {
next if $opt !~ m/^usb\d+$/;
+ check_usb_perm($rpcenv, $authuser, $vmid, $pool, $opt, $param->{$opt});
+ }
- if ($param->{$opt} =~ m/spice/) {
- $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
- } else {
- die "only root can set '$opt' config for real devices\n";
- }
+ return 1;
+};
+
+my sub check_hostpci_perm {
+ my ($rpcenv, $authuser, $vmid, $pool, $opt, $value) = @_;
+
+ return 1 if $authuser eq 'root@pam';
+
+ my $device = PVE::JSONSchema::parse_property_string('pve-qm-hostpci', $value);
+ if ($device->{host}) {
+ die "only root can set '$opt' config for non-mapped devices\n";
+ } elsif ($device->{mapping}) {
+ $rpcenv->check_full($authuser, "/mapping/pci/$device->{mapping}", ['Mapping.Use']);
+ $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
+ } else {
+ die "either 'host' or 'mapping' must be set.\n";
+ }
+
+ return 1;
+}
+
+my sub check_vm_create_hostpci_perm {
+ my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
+
+ return 1 if $authuser eq 'root@pam';
+
+ foreach my $opt (keys %{$param}) {
+ next if $opt !~ m/^hostpci\d+$/;
+ check_hostpci_perm($rpcenv, $authuser, $vmid, $pool, $opt, $param->{$opt});
}
return 1;
# else, as there the permission can be value dependend
next if PVE::QemuServer::is_valid_drivename($opt);
next if $opt eq 'cdrom';
- next if $opt =~ m/^(?:unused|serial|usb)\d+$/;
+ next if $opt =~ m/^(?:unused|serial|usb|hostpci)\d+$/;
next if $opt eq 'tags';
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']);
} elsif ($diskoptions->{$opt}) {
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
- } elsif ($opt =~ m/^(?:net|ipconfig)\d+$/) {
+ } elsif ($opt =~ m/^net\d+$/) {
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
- } elsif ($cloudinitoptions->{$opt}) {
+ } elsif ($cloudinitoptions->{$opt} || $opt =~ m/^ipconfig\d+$/) {
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Cloudinit', 'VM.Config.Network'], 1);
} elsif ($opt eq 'vmstate') {
# the user needs Disk and PowerMgmt privileges to change the vmstate
# also needs privileges on the storage, that will be checked later
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk', 'VM.PowerMgmt' ]);
} else {
- # catches hostpci\d+, args, lock, etc.
+ # catches args, lock, etc.
# new options will be checked here
die "only root can set '$opt' config\n";
}
return 1;
};
+sub assert_scsi_feature_compatibility {
+ my ($opt, $conf, $storecfg, $drive_attributes) = @_;
+
+ my $drive = PVE::QemuServer::Drive::parse_drive($opt, $drive_attributes, 1);
+
+ my $machine_type = PVE::QemuServer::get_vm_machine($conf, undef, $conf->{arch});
+ my $machine_version = PVE::QemuServer::Machine::extract_version(
+ $machine_type, PVE::QemuServer::kvm_user_version());
+ my $drivetype = PVE::QemuServer::Drive::get_scsi_device_type(
+ $drive, $storecfg, $machine_version);
+
+ if ($drivetype ne 'hd' && $drivetype ne 'cd') {
+ if ($drive->{product}) {
+ raise_param_exc({
+ $opt => "Passing of product information is only supported for 'scsi-hd' and "
+ ."'scsi-cd' devices (e.g. not pass-through).",
+ });
+ }
+ if ($drive->{vendor}) {
+ raise_param_exc({
+ $opt => "Passing of vendor information is only supported for 'scsi-hd' and "
+ ."'scsi-cd' devices (e.g. not pass-through).",
+ });
+ }
+ }
+}
+
__PACKAGE__->register_method({
name => 'vmlist',
path => '',
my ($archive_storeid, $archive_volname) = PVE::Storage::parse_volume_id($archive, 1);
+ my $res = {};
+
if (defined($archive_storeid)) {
my $scfg = PVE::Storage::storage_config($storecfg, $archive_storeid);
+ $res->{volid} = $archive;
if ($scfg->{type} eq 'pbs') {
- return {
- type => 'pbs',
- volid => $archive,
- };
+ $res->{type} = 'pbs';
+ return $res;
}
}
my $path = PVE::Storage::abs_filesystem_path($storecfg, $archive);
- return {
- type => 'file',
- path => $path,
- };
+ $res->{type} = 'file';
+ $res->{path} = $path;
+ return $res;
};
-
__PACKAGE__->register_method({
name => 'create_vm',
path => '',
permissions => {
description => "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
"For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
- "If you create disks you need 'Datastore.AllocateSpace' on any used storage.",
+ "If you create disks you need 'Datastore.AllocateSpace' on any used storage." .
+ "If you use a bridge/vlan, you need 'SDN.Use' on any used bridge/vlan.",
user => 'all', # check inside
},
protected => 1,
'live-restore' => {
optional => 1,
type => 'boolean',
- description => "Start the VM immediately from the backup and restore in background. PBS only.",
- requires => 'archive',
+ description => "Start the VM immediately while importing or restoring in the background.",
},
pool => {
optional => 1,
&$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
&$check_vm_create_serial_perm($rpcenv, $authuser, $vmid, $pool, $param);
- &$check_vm_create_usb_perm($rpcenv, $authuser, $vmid, $pool, $param);
+ check_vm_create_usb_perm($rpcenv, $authuser, $vmid, $pool, $param);
+ check_vm_create_hostpci_perm($rpcenv, $authuser, $vmid, $pool, $param);
+ PVE::QemuServer::check_bridge_access($rpcenv, $authuser, $param);
&$check_cpu_model_access($rpcenv, $authuser, $param);
$check_drive_param->($param, $storecfg);
live => $live_restore,
override_conf => $param,
};
+ if (my $volid = $archive->{volid}) {
+ # best effort, real check is after restoring!
+ my $merged = eval {
+ my $old_conf = PVE::Storage::extract_vzdump_config($storecfg, $volid);
+ PVE::QemuServer::restore_merge_config("backup/qemu-server/$vmid.conf", $old_conf, $param);
+ };
+ if ($@) {
+ warn "Could not extract backed up config: $@\n";
+ warn "Skipping early checks!\n";
+ } else {
+ PVE::QemuServer::check_restore_permissions($rpcenv, $authuser, $merged);
+ }
+ }
if ($archive->{type} eq 'file' || $archive->{type} eq 'pipe') {
die "live-restore is only compatible with backup images from a Proxmox Backup Server\n"
if $live_restore;
eval { PVE::QemuServer::template_create($vmid, $restored_conf) };
warn $@ if $@;
}
+
+ PVE::QemuServer::create_ifaces_ipams_ips($restored_conf, $vmid) if $unique;
};
# ensure no old replication state are exists
};
my $createfn = sub {
+ my $live_import_mapping = {};
+
# ensure no old replication state are exists
PVE::ReplicationState::delete_guest_states($vmid);
my $conf = $param;
my $arch = PVE::QemuServer::get_vm_arch($conf);
+ for my $opt (sort keys $param->%*) {
+ next if $opt !~ m/^scsi\d+$/;
+ assert_scsi_feature_compatibility($opt, $conf, $storecfg, $param->{$opt});
+ }
+
$conf->{meta} = PVE::QemuServer::new_meta_info_string();
my $vollist = [];
eval {
- ($vollist, my $created_opts) = $create_disks->(
+ ($vollist, my $created_opts, $live_import_mapping) = create_disks(
$rpcenv,
$authuser,
$conf,
$pool,
$param,
$storage,
+ $live_restore,
);
$conf->{$_} = $created_opts->{$_} for keys $created_opts->%*;
$conf->{boot} = PVE::QemuServer::print_bootorder($devs);
}
+ my $vga = PVE::QemuServer::parse_vga($conf->{vga});
+ PVE::QemuServer::assert_clipboard_config($vga);
+
# auto generate uuid if user did not specify smbios1 option
if (!$conf->{smbios1}) {
$conf->{smbios1} = PVE::QemuServer::generate_smbios1_uuid();
$conf->{vmgenid} = PVE::QemuServer::generate_uuid();
}
- my $machine = $conf->{machine};
+ my $machine_conf = PVE::QemuServer::Machine::parse_machine($conf->{machine});
+ my $machine = $machine_conf->{type};
if (!$machine || $machine =~ m/^(?:pc|q35|virt)$/) {
# always pin Windows' machine version on create, they get to easily confused
if (PVE::QemuServer::Helpers::windows_version($conf->{ostype})) {
- $conf->{machine} = PVE::QemuServer::windows_get_pinned_machine_version($machine);
+ $machine_conf->{type} = PVE::QemuServer::windows_get_pinned_machine_version($machine);
+ $conf->{machine} = PVE::QemuServer::Machine::print_machine($machine_conf);
}
}
- PVE::QemuConfig->write_config($vmid, $conf);
+ $conf->{lock} = 'import' if $live_import_mapping;
+ PVE::QemuConfig->write_config($vmid, $conf);
};
my $err = $@;
}
PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
+
+ PVE::QemuServer::create_ifaces_ipams_ips($conf, $vmid);
};
PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd);
- if ($start_after_create) {
+ if ($start_after_create && !$live_restore) {
print "Execute autostart\n";
eval { PVE::API2::Qemu->vm_start({vmid => $vmid, node => $node}) };
warn $@ if $@;
+ return;
+ } else {
+ return $live_import_mapping;
}
};
} else {
$worker_name = 'qmcreate';
$code = sub {
- eval { $createfn->() };
+ # If a live import was requested the create function returns
+ # the mapping for the startup.
+ my $live_import_mapping = eval { $createfn->() };
if (my $err = $@) {
eval {
my $conffile = PVE::QemuConfig->config_file($vmid);
warn $@ if $@;
die $err;
}
+
+ if ($live_import_mapping) {
+ my $import_options = {
+ bwlimit => $bwlimit,
+ live => 1,
+ };
+
+ my $conf = PVE::QemuConfig->load_config($vmid);
+ PVE::QemuServer::live_import_from_files(
+ $live_import_mapping,
+ $vmid,
+ $conf,
+ $import_options,
+ );
+ }
};
}
description => "Configuration option name.",
type => 'string',
},
- old => {
+ value => {
description => "Value as it was used to generate the current cloudinit image.",
type => 'string',
optional => 1,
},
- new => {
+ pending => {
description => "The new pending value.",
type => 'string',
optional => 1,
},
+ delete => {
+ description => "Indicates a pending delete request if present and not 0. ",
+ type => 'integer',
+ minimum => 0,
+ maximum => 1,
+ optional => 1,
+ },
},
},
},
my $ci = $conf->{cloudinit};
- my $res = {};
+ $conf->{cipassword} = '**********' if exists $conf->{cipassword};
+ $ci->{cipassword} = '**********' if exists $ci->{cipassword};
+
+ my $res = [];
+
+ # All the values that got added
my $added = delete($ci->{added}) // '';
for my $key (PVE::Tools::split_list($added)) {
- $res->{$key} = { new => $conf->{$key} };
+ push @$res, { key => $key, pending => $conf->{$key} };
}
- for my $key (keys %$ci) {
- if (!exists($conf->{$key})) {
- $res->{$key} = { old => $ci->{$key} };
+ # All already existing values (+ their new value, if it exists)
+ for my $opt (keys %$cloudinitoptions) {
+ next if !$conf->{$opt};
+ next if $added =~ m/$opt/;
+ my $item = {
+ key => $opt,
+ };
+
+ if (my $pending = $ci->{$opt}) {
+ $item->{value} = $pending;
+ $item->{pending} = $conf->{$opt};
} else {
- $res->{$key} = {
- old => $ci->{$key},
- new => $conf->{$key},
- };
+ $item->{value} = $conf->{$opt},
}
+
+ push @$res, $item;
}
- if (defined(my $pw = $res->{cipassword})) {
- $pw->{old} = '**********' if exists $pw->{old};
- $pw->{new} = '**********' if exists $pw->{new};
+ # Now, we'll find the deleted ones
+ for my $opt (keys %$ci) {
+ next if $conf->{$opt};
+ push @$res, { key => $opt, delete => 1 };
}
return $res;
proxyto => 'node',
description => "Regenerate and change cloudinit config drive.",
permissions => {
- check => ['perm', '/vms/{vmid}', 'VM.Config.Cloudinit'],
+ check => ['perm', '/vms/{vmid}', ['VM.Config.Cloudinit']],
},
parameters => {
additionalProperties => 0,
my $storecfg = PVE::Storage::config();
- my $defaults = PVE::QemuServer::load_defaults();
-
&$resolve_cdrom_alias($param);
# now try to verify all parameters
return if defined($volname) && $volname eq 'cloudinit';
my $format;
- if ($volid =~ $NEW_DISK_RE) {
+ if ($volid =~ $PVE::QemuServer::Drive::NEW_DISK_RE) {
$storeid = $2;
$format = $drive->{format} || PVE::Storage::storage_default_format($storecfg, $storeid);
} else {
&$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
+ PVE::QemuServer::check_bridge_access($rpcenv, $authuser, $param);
+
my $updatefn = sub {
my $conf = PVE::QemuConfig->load_config($vmid);
}
if ($param->{memory} || defined($param->{balloon})) {
- my $maxmem = $param->{memory} || $conf->{pending}->{memory} || $conf->{memory} || $defaults->{memory};
+
+ my $memory = $param->{memory} || $conf->{pending}->{memory} || $conf->{memory};
+ my $maxmem = get_current_memory($memory);
my $balloon = defined($param->{balloon}) ? $param->{balloon} : $conf->{pending}->{balloon} || $conf->{balloon};
die "balloon value too large (must be smaller than assigned memory)\n"
PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
PVE::QemuConfig->write_config($vmid, $conf);
} elsif ($opt =~ m/^usb\d+$/) {
- if ($val =~ m/spice/) {
- $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
- } elsif ($authuser ne 'root@pam') {
- die "only root can delete '$opt' config for real devices\n";
- }
+ check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $val);
+ PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
+ PVE::QemuConfig->write_config($vmid, $conf);
+ } elsif ($opt =~ m/^hostpci\d+$/) {
+ check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $val);
PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
PVE::QemuConfig->write_config($vmid, $conf);
} elsif ($opt eq 'tags') {
assert_tag_permissions($vmid, $val, '', $rpcenv, $authuser);
delete $conf->{$opt};
PVE::QemuConfig->write_config($vmid, $conf);
+ } elsif ($opt =~ m/^net\d+$/) {
+ if ($conf->{$opt}) {
+ PVE::QemuServer::check_bridge_access(
+ $rpcenv,
+ $authuser,
+ { $opt => $conf->{$opt} },
+ );
+ }
+ PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
+ PVE::QemuConfig->write_config($vmid, $conf);
} else {
PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
PVE::QemuConfig->write_config($vmid, $conf);
PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}))
if defined($conf->{pending}->{$opt});
- my (undef, $created_opts) = $create_disks->(
+ assert_scsi_feature_compatibility($opt, $conf, $storecfg, $param->{$opt})
+ if $opt =~ m/^scsi\d+$/;
+
+ my (undef, $created_opts) = create_disks(
$rpcenv,
$authuser,
$conf,
$vmid,
undef,
{$opt => $param->{$opt}},
+ undef,
+ undef,
);
$conf->{pending}->{$_} = $created_opts->{$_} for keys $created_opts->%*;
die "only root can modify '$opt' config for real devices\n";
}
$conf->{pending}->{$opt} = $param->{$opt};
+ } elsif ($opt eq 'vga') {
+ my $vga = PVE::QemuServer::parse_vga($param->{$opt});
+ PVE::QemuServer::assert_clipboard_config($vga);
+ $conf->{pending}->{$opt} = $param->{$opt};
} elsif ($opt =~ m/^usb\d+/) {
- if ((!defined($conf->{$opt}) || $conf->{$opt} =~ m/spice/) && $param->{$opt} =~ m/spice/) {
- $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
- } elsif ($authuser ne 'root@pam') {
- die "only root can modify '$opt' config for real devices\n";
+ if (my $olddevice = $conf->{$opt}) {
+ check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $conf->{$opt});
}
+ check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
+ $conf->{pending}->{$opt} = $param->{$opt};
+ } elsif ($opt =~ m/^hostpci\d+$/) {
+ if (my $oldvalue = $conf->{$opt}) {
+ check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $oldvalue);
+ }
+ check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
$conf->{pending}->{$opt} = $param->{$opt};
} elsif ($opt eq 'tags') {
assert_tag_permissions($vmid, $conf->{$opt}, $param->{$opt}, $rpcenv, $authuser);
$conf->{pending}->{$opt} = PVE::GuestHelpers::get_unique_tags($param->{$opt});
+ } elsif ($opt =~ m/^net\d+$/) {
+ if ($conf->{$opt}) {
+ PVE::QemuServer::check_bridge_access(
+ $rpcenv,
+ $authuser,
+ { $opt => $conf->{$opt} },
+ );
+ }
+ $conf->{pending}->{$opt} = $param->{$opt};
+ } elsif ($opt eq 'machine') {
+ my $machine_conf = PVE::QemuServer::Machine::parse_machine($param->{$opt});
+ $conf->{pending}->{$opt} = $param->{$opt};
} else {
$conf->{pending}->{$opt} = $param->{$opt};
websocket => {
optional => 1,
type => 'boolean',
- description => "starts websockify instead of vncproxy",
+ description => "Prepare for websocket upgrade (only required when using "
+ ."serial terminal, otherwise upgrade is always possible).",
},
'generate-password' => {
optional => 1,
} else {
- $ENV{LC_PVE_TICKET} = $password if $websocket; # set ticket with "qm vncproxy"
+ $ENV{LC_PVE_TICKET} = $password; # set ticket with "qm vncproxy"
$cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
type => 'boolean',
optional => 1,
},
+ clipboard => {
+ description => 'Enable a specific clipboard. If not set, depending on'
+ .' the display type the SPICE one will be added.',
+ type => 'string',
+ enum => ['vnc'],
+ optional => 1,
+ },
},
},
code => sub {
my $spice = defined($vga->{type}) && $vga->{type} =~ /^virtio/;
$spice ||= PVE::QemuServer::vga_conf_has_spice($conf->{vga});
$status->{spice} = 1 if $spice;
+ $status->{clipboard} = $vga->{clipboard};
}
$status->{agent} = 1 if PVE::QemuServer::get_qga_key($conf, 'enabled');
my $shutdown = 1;
- # if vm is paused, do not shutdown (but stop if forceStop = 1)
- # otherwise, we will infer a shutdown command, but run into the timeout,
- # then when the vm is resumed, it will instantly shutdown
- #
- # checking the qmp status here to get feedback to the gui/cli/api
- # and the status query should not take too long
- if (PVE::QemuServer::vm_is_paused($vmid)) {
+ # sending a graceful shutdown command to paused VMs runs into timeouts, and even worse, when
+ # the VM gets resumed later, it still gets the request delivered and powers off
+ if (PVE::QemuServer::vm_is_paused($vmid, 1)) {
if ($param->{forceStop}) {
warn "VM is paused - stop instead of shutdown\n";
$shutdown = 0;
my $node = extract_param($param, 'node');
my $vmid = extract_param($param, 'vmid');
- die "VM is paused - cannot shutdown\n" if PVE::QemuServer::vm_is_paused($vmid);
+ die "VM is paused - cannot shutdown\n" if PVE::QemuServer::vm_is_paused($vmid, 1);
die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
permissions => {
description => "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
"on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
- "'Datastore.AllocateSpace' on any used storage.",
+ "'Datastore.AllocateSpace' on any used storage and 'SDN.Use' on any used bridge/vnet",
check =>
[ 'and',
['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
+ PVE::QemuServer::check_mapping_access($rpcenv, $authuser, $oldconf);
+
+ PVE::QemuServer::check_bridge_access($rpcenv, $authuser, $oldconf);
die "can't clone VM to node '$target' (VM uses local storage)\n"
if $target && !$sharedvm;
PVE::QemuConfig->write_config($newid, $newconf);
+ PVE::QemuServer::create_ifaces_ipams_ips($newconf, $newid);
+
if ($target) {
- # always deactivate volumes - avoid lvm LVs to be active on several nodes
- PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running;
+ if (!$running) {
+ # always deactivate volumes – avoids that LVM LVs are active on several nodes
+ eval { PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) };
+ # but only warn when that fails (e.g., parallel clones keeping them active)
+ log_warn($@) if $@;
+ }
+
PVE::Storage::deactivate_volumes($storecfg, $newvollist);
my $newconffile = PVE::QemuConfig->config_file($newid, $target);
local_resources => {
type => 'array',
description => "List local resources e.g. pci, usb"
- }
+ },
+ 'mapped-resources' => {
+ type => 'array',
+ description => "List of mapped resources e.g. pci, usb"
+ },
},
},
code => sub {
$res->{running} = PVE::QemuServer::check_running($vmid) ? 1:0;
- # if vm is not running, return target nodes where local storage is available
+ my ($local_resources, $mapped_resources, $missing_mappings_by_node) =
+ PVE::QemuServer::check_local_resources($vmconf, 1);
+ delete $missing_mappings_by_node->{$localnode};
+
+ my $vga = PVE::QemuServer::parse_vga($vmconf->{vga});
+ if ($res->{running} && $vga->{'clipboard'} && $vga->{'clipboard'} eq 'vnc') {
+ push $local_resources->@*, "clipboard=vnc";
+ }
+
+ # if vm is not running, return target nodes where local storage/mapped devices are available
# for offline migration
if (!$res->{running}) {
$res->{allowed_nodes} = [];
delete $checked_nodes->{$localnode};
foreach my $node (keys %$checked_nodes) {
- if (!defined $checked_nodes->{$node}->{unavailable_storages}) {
+ my $missing_mappings = $missing_mappings_by_node->{$node};
+ if (scalar($missing_mappings->@*)) {
+ $checked_nodes->{$node}->{'unavailable-resources'} = $missing_mappings;
+ next;
+ }
+
+ if (!defined($checked_nodes->{$node}->{unavailable_storages})) {
push @{$res->{allowed_nodes}}, $node;
}
$res->{not_allowed_nodes} = $checked_nodes;
}
-
my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
$res->{local_disks} = [ values %$local_disks ];;
- my $local_resources = PVE::QemuServer::check_local_resources($vmconf, 1);
-
$res->{local_resources} = $local_resources;
+ $res->{'mapped-resources'} = $mapped_resources;
return $res;
},
},
},
- returns => { type => 'null'},
+ returns => {
+ type => 'string',
+ description => "the task ID.",
+ },
code => sub {
my ($param) = @_;
my (undef, undef, undef, undef, undef, undef, $format) =
PVE::Storage::parse_volname($storecfg, $drive->{file});
- die "can't resize volume: $disk if snapshot exists\n"
- if %{$conf->{snapshots}} && $format eq 'qcow2';
-
my $volid = $drive->{file};
die "disk '$disk' has no associated volume\n" if !$volid;
PVE::QemuConfig->write_config($vmid, $conf);
};
- PVE::QemuConfig->lock_config($vmid, $updatefn);
- return;
+ my $worker = sub {
+ PVE::QemuConfig->lock_config($vmid, $updatefn);
+ };
+
+ return $rpcenv->fork_worker('resize', $vmid, $authuser, $worker);
}});
__PACKAGE__->register_method({
'disk' => [
undef,
$storeid,
- undef,
$drive,
0,
$format,