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;
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";
}
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;
};
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,
&$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;
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
&$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);
}
$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};
} 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];
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
+ # 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)) {
if ($param->{forceStop}) {
warn "VM is paused - stop instead of shutdown\n";
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;
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};
+
+ # 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,