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::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']);
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;
cipassword => 1,
citype => 1,
ciuser => 1,
+ ciupgrade => 1,
nameserver => 1,
searchdomain => 1,
sshkeys => 1,
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_devicetype(
+ $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 => '',
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 $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 = [];
$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();
}
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);
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 {
}
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"
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});
+ assert_scsi_feature_compatibility($opt, $conf, $storecfg, $param->{$opt})
+ if $opt =~ m/^scsi\d+$/;
+
my (undef, $created_opts) = $create_disks->(
$rpcenv,
$authuser,
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 (my $olddevice = $conf->{$opt}) {
check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $conf->{$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};
} 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);
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);
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}) {
'disk' => [
undef,
$storeid,
- undef,
$drive,
0,
$format,