use Socket qw(SOCK_STREAM);
use PVE::APIClient::LWP;
+use PVE::CGroup;
use PVE::Cluster qw (cfs_read_file cfs_write_file);;
use PVE::RRD;
use PVE::SafeSyslog;
use PVE::QemuServer::ImportDisk;
use PVE::QemuServer::Monitor qw(mon_cmd);
use PVE::QemuServer::Machine;
+use PVE::QemuServer::PCI;
+use PVE::QemuServer::USB;
use PVE::QemuMigrate;
use PVE::RPCEnvironment;
use PVE::AccessControl;
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,
PVE::Tools::validate_ssh_public_keys($ssh_keys);
}
- $param->{cpuunits} = PVE::GuestHelpers::get_cpuunits($param->{cpuunits})
+ $param->{cpuunits} = PVE::CGroup::clamp_cpu_shares($param->{cpuunits})
if defined($param->{cpuunits}); # clamp value depending on cgroup version
PVE::Cluster::check_cfs_quorum();
# OK
} elsif ($archive && $force && (-f $filename) &&
$rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
- # OK: user has VM.Backup permissions, and want to restore an existing VM
+ # OK: user has VM.Backup permissions and wants to restore an existing VM
} else {
raise_perm_exc();
}
&$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;
PVE::Tools::validate_ssh_public_keys($ssh_keys);
}
- $param->{cpuunits} = PVE::GuestHelpers::get_cpuunits($param->{cpuunits})
+ $param->{cpuunits} = PVE::CGroup::clamp_cpu_shares($param->{cpuunits})
if defined($param->{cpuunits}); # clamp value depending on cgroup version
die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
&$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);
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') {
}
$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} = $param->{$opt};
+ $conf->{pending}->{$opt} = PVE::GuestHelpers::get_unique_tags($param->{$opt});
} else {
$conf->{pending}->{$opt} = $param->{$opt};
type => 'object',
},
spice => {
- description => "Qemu VGA configuration supports spice.",
+ description => "QEMU VGA configuration supports spice.",
type => 'boolean',
optional => 1,
},
agent => {
- description => "Qemu GuestAgent enabled in config.",
+ description => "QEMU Guest Agent is enabled in config.",
type => 'boolean',
optional => 1,
},
raise_param_exc({ skiplock => "Only root may use this option." })
if $skiplock && $authuser ne 'root@pam';
+ # nocheck is used as part of migration when config file might be still
+ # be on source node
my $nocheck = extract_param($param, 'nocheck');
raise_param_exc({ nocheck => "Only root may use this option." })
if $nocheck && $authuser ne 'root@pam';
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;
$param->{online} = 0;
}
- # FIXME: fork worker hear to avoid timeout? or poll these periodically
- # in pvestatd and access cached info here? all of the below is actually
- # checked at the remote end anyway once we call the mtunnel endpoint,
- # we could also punt it to the client and not do it here at all..
- my $resources = $api_client->get("/cluster/resources", { type => 'vm' });
- if (grep { defined($_->{vmid}) && $_->{vmid} eq $target_vmid } @$resources) {
- raise_param_exc({ target_vmid => "Guest with ID '$target_vmid' already exists on remote cluster" });
- }
-
- my $storages = $api_client->get("/nodes/localhost/storage", { enabled => 1 });
-
my $storecfg = PVE::Storage::config();
my $target_storage = extract_param($param, 'target-storage');
my $storagemap = eval { PVE::JSONSchema::parse_idmap($target_storage, 'pve-storage-id') };
raise_param_exc({ 'target-bridge' => "failed to parse bridge map: $@" })
if $@;
- my $check_remote_storage = sub {
- my ($storage) = @_;
- my $found = [ grep { $_->{storage} eq $storage } @$storages ];
- die "remote: storage '$storage' does not exist!\n"
- if !@$found;
-
- $found = @$found[0];
-
- my $content_types = [ PVE::Tools::split_list($found->{content}) ];
- die "remote: storage '$storage' cannot store images\n"
- if !grep { $_ eq 'images' } @$content_types;
- };
-
- foreach my $target_sid (values %{$storagemap->{entries}}) {
- $check_remote_storage->($target_sid);
- }
-
- $check_remote_storage->($storagemap->{default})
- if $storagemap->{default};
-
die "remote migration requires explicit storage mapping!\n"
if $storagemap->{identity};
method => 'POST',
protected => 1,
proxyto => 'node',
- description => "Execute Qemu monitor commands.",
+ description => "Execute QEMU monitor commands.",
permissions => {
description => "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
},
},
},
- 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({
snapname => get_standard_option('pve-snapshot-name'),
start => {
type => 'boolean',
- description => "Whether the VM should get started after rolling back successfully",
+ description => "Whether the VM should get started after rolling back successfully."
+ . " (Note: VMs will be automatically started if the snapshot includes RAM.)",
optional => 1,
default => 0,
},
PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
PVE::QemuConfig->snapshot_rollback($vmid, $snapname);
- if ($param->{start}) {
+ if ($param->{start} && !PVE::QemuServer::Helpers::vm_running_locally($vmid)) {
PVE::API2::Qemu->vm_start({ vmid => $vmid, node => $node });
}
};