add PCI/USB Mapping configs

adds a config file for each type of resource (usb/pci) by using a 'map'
array propertystring for each node mapping

in each mapping we save the path(s) and some other information to detect
hardware changes (if possible) like the vendor/device id

both configs have custom header parser/formatter to omit the type (since
we only want one type per config here)

also each config has some helpers like find_on_current_node

the resulting config (e.g. for pci) would look like this:
This commit is contained in:
Dominik Csapak 2023-06-06 15:52:01 +02:00 committed by Thomas Lamprecht
parent aabf879e0c
commit 11fa95682e
3 changed files with 412 additions and 0 deletions

View File

@ -14,6 +14,9 @@ install: PVE
install -m 0644 PVE/Replication.pm ${PERL5DIR}/PVE/
install -m 0644 PVE/StorageTunnel.pm ${PERL5DIR}/PVE/
install -m 0644 PVE/Tunnel.pm ${PERL5DIR}/PVE/
install -d ${PERL5DIR}/PVE/Mapping
install -m 0644 PVE/Mapping/PCI.pm ${PERL5DIR}/PVE/Mapping/
install -m 0644 PVE/Mapping/USB.pm ${PERL5DIR}/PVE/Mapping/
install -d ${PERL5DIR}/PVE/VZDump
install -m 0644 PVE/VZDump/Plugin.pm ${PERL5DIR}/PVE/VZDump/
install -m 0644 PVE/VZDump/Common.pm ${PERL5DIR}/PVE/VZDump/

226
src/PVE/Mapping/PCI.pm Normal file
View File

@ -0,0 +1,226 @@
package PVE::Mapping::PCI;
use strict;
use warnings;
use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_lock_file cfs_write_file);
use PVE::INotify;
use PVE::JSONSchema qw(get_standard_option parse_property_string);
use PVE::SectionConfig;
use PVE::SysFSTools;
use base qw(PVE::SectionConfig);
my $FILENAME = 'mapping/pci.cfg';
cfs_register_file($FILENAME,
sub { __PACKAGE__->parse_config(@_); },
sub { __PACKAGE__->write_config(@_); });
# so we don't have to repeat the type every time
sub parse_section_header {
my ($class, $line) = @_;
if ($line =~ m/^(\S+)\s*$/) {
my $id = $1;
my $errmsg = undef; # set if you want to skip whole section
eval { PVE::JSONSchema::pve_verify_configid($id) };
$errmsg = $@ if $@;
my $config = {}; # to return additional attributes
return ('pci', $id, $errmsg, $config);
}
return undef;
}
sub format_section_header {
my ($class, $type, $sectionId, $scfg, $done_hash) = @_;
return "$sectionId\n";
}
sub type {
return 'pci';
}
my $PCI_RE = "[a-f0-9]{4,}:[a-f0-9]{2}:[a-f0-9]{2}(?:\.[a-f0-9])?";
my $map_fmt = {
node => get_standard_option('pve-node'),
id =>{
description => "The vendor and device ID that is expected. Used for".
" detecting hardware changes",
type => 'string',
pattern => qr/^[0-9A-Fa-f]{4}:[0-9A-Fa-f]{4}$/,
},
'subsystem-id' => {
description => "The subsystem vendor and device ID that is expected. Used".
" for detecting hardware changes.",
type => 'string',
pattern => qr/^[0-9A-Fa-f]{4}:[0-9A-Fa-f]{4}$/,
optional => 1,
},
path => {
description => "The path to the device. If the function is omitted, the whole device is"
." mapped. In that case use the attributes of the first device. You can give"
." multiple paths as a semicolon seperated list, the first available will then"
." be chosen on guest start.",
type => 'string',
pattern => "(?:${PCI_RE};)*${PCI_RE}",
},
iommugroup => {
type => 'integer',
description => "The IOMMU group in which the device is to be expected in.".
"Used for detecting hardware changes.",
optional => 1,
},
description => {
description => "Description of the node specific device.",
type => 'string',
optional => 1,
maxLength => 4096,
},
};
my $defaultData = {
propertyList => {
id => {
type => 'string',
description => "The ID of the logical PCI mapping.",
format => 'pve-configid',
},
description => {
description => "Description of the logical PCI device.",
type => 'string',
optional => 1,
maxLength => 4096,
},
mdev => {
type => 'boolean',
optional => 1,
},
map => {
type => 'array',
description => 'A list of maps for the cluster nodes.',
optional => 1,
items => {
type => 'string',
format => $map_fmt,
},
},
},
};
sub private {
return $defaultData;
}
sub options {
return {
description => { optional => 1 },
mdev => { optional => 1 },
map => {},
};
}
# checks if the given config is valid for the current node
sub assert_valid {
my ($name, $cfg) = @_;
my @paths = split(';', $cfg->{path} // '');
my $idx = 0;
for my $path (@paths) {
my $multifunction = 0;
if ($path !~ m/\.[a-f0-9]/i) {
# whole device, add .0 (must exist)
$path = "$path.0";
$multifunction = 1;
}
my $info = PVE::SysFSTools::pci_device_info($path, 1);
die "pci device '$path' not found\n" if !defined($info);
my $correct_props = {
id => "$info->{vendor}:$info->{device}",
iommugroup => $info->{iommugroup},
};
if (defined($info->{'subsystem_vendor'}) && defined($info->{'subsystem_device'})) {
$correct_props->{'subsystem-id'} = "$info->{'subsystem_vendor'}:$info->{'subsystem_device'}";
}
for my $prop (sort keys %$correct_props) {
next if $prop eq 'iommugroup' && $idx > 0; # check iommu only on the first device
next if !defined($correct_props->{$prop}) && !defined($cfg->{$prop});
die "no '$prop' for device '$path'\n"
if defined($correct_props->{$prop}) && !defined($cfg->{$prop});
die "'$prop' configured but should not be\n"
if !defined($correct_props->{$prop}) && defined($cfg->{$prop});
my $correct_prop = $correct_props->{$prop};
$correct_prop =~ s/0x//g;
my $configured_prop = $cfg->{$prop};
$configured_prop =~ s/0x//g;
die "'$prop' does not match for '$name' ($correct_prop != $configured_prop)\n"
if $correct_prop ne $configured_prop;
}
$idx++;
}
return 1;
};
sub config {
return cfs_read_file($FILENAME);
}
sub lock_pci_config {
my ($code, $errmsg) = @_;
cfs_lock_file($FILENAME, undef, $code);
if (my $err = $@) {
$errmsg ? die "$errmsg: $err" : die $err;
}
}
sub write_pci_config {
my ($cfg) = @_;
cfs_write_file($FILENAME, $cfg);
}
sub find_on_current_node {
my ($id) = @_;
my $cfg = PVE::Mapping::PCI::config();
my $node = PVE::INotify::nodename();
# ignore errors
return get_node_mapping($cfg, $id, $node);
}
sub get_node_mapping {
my ($cfg, $id, $nodename) = @_;
return undef if !defined($cfg->{ids}->{$id});
my $res = [];
for my $map ($cfg->{ids}->{$id}->{map}->@*) {
my $entry = eval { parse_property_string($map_fmt, $map) };
warn $@ if $@;
if ($entry && $entry->{node} eq $nodename) {
push $res->@*, $entry;
}
}
return $res;
}
PVE::Mapping::PCI->register();
PVE::Mapping::PCI->init();
1;

183
src/PVE/Mapping/USB.pm Normal file
View File

@ -0,0 +1,183 @@
package PVE::Mapping::USB;
use strict;
use warnings;
use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_lock_file cfs_write_file);
use PVE::INotify;
use PVE::JSONSchema qw(get_standard_option parse_property_string);
use PVE::SectionConfig;
use PVE::SysFSTools;
use base qw(PVE::SectionConfig);
my $FILENAME = 'mapping/usb.cfg';
cfs_register_file($FILENAME,
sub { __PACKAGE__->parse_config(@_); },
sub { __PACKAGE__->write_config(@_); });
# so we don't have to repeat the type every time
sub parse_section_header {
my ($class, $line) = @_;
if ($line =~ m/^(\S+)\s*$/) {
my $id = $1;
my $errmsg = undef; # set if you want to skip whole section
eval { PVE::JSONSchema::pve_verify_configid($id) };
$errmsg = $@ if $@;
my $config = {}; # to return additional attributes
return ('usb', $id, $errmsg, $config);
}
return undef;
}
sub format_section_header {
my ($class, $type, $sectionId, $scfg, $done_hash) = @_;
return "$sectionId\n";
}
sub type {
return 'usb';
}
my $map_fmt = {
node => get_standard_option('pve-node'),
'id' => {
description => "The vendor and device ID that is expected. If a USB path".
" is given, it is only used for detecting hardware changes",
type => 'string',
pattern => qr/^[0-9A-Fa-f]{4}:[0-9A-Fa-f]{4}$/,
},
path => {
description => "The path to the usb device.",
type => 'string',
optional => 1,
pattern => qr/^(\d+)\-(\d+(\.\d+)*)$/,
},
description => {
description => "Description of the node specific device.",
type => 'string',
optional => 1,
maxLength => 4096,
},
};
my $defaultData = {
propertyList => {
id => {
type => 'string',
description => "The ID of the logical PCI mapping.",
format => 'pve-configid',
},
description => {
description => "Description of the logical PCI device.",
type => 'string',
optional => 1,
maxLength => 4096,
},
map => {
type => 'array',
description => 'A list of maps for the cluster nodes.',
items => {
type => 'string',
format => $map_fmt,
},
},
},
};
sub private {
return $defaultData;
}
sub options {
return {
description => { optional => 1 },
map => {},
};
}
# checks if the given device is valid for the current node
sub assert_valid {
my ($name, $cfg) = @_;
my $id = $cfg->{id};
my $usb_list = PVE::SysFSTools::scan_usb();
my $info;
if (my $path = $cfg->{path}) {
for my $dev (@$usb_list) {
next if !$dev->{usbpath} || !$dev->{busnum};
my $usbpath = "$dev->{busnum}-$dev->{usbpath}";
next if $usbpath ne $path;
$info = $dev;
}
die "usb device '$path' not found\n" if !defined($info);
my $realId = "$info->{vendid}:$info->{prodid}";
die "'id' does not match for '$name' ($realId != $id)\n"
if $realId ne $id;
} else {
for my $dev (@$usb_list) {
my $realId = "$dev->{vendid}:$dev->{prodid}";
next if $realId ne $id;
$info = $dev;
}
die "usb device '$id' not found\n" if !defined($info);
}
return 1;
};
sub config {
return cfs_read_file($FILENAME);
}
sub lock_usb_config {
my ($code, $errmsg) = @_;
cfs_lock_file($FILENAME, undef, $code);
if (my $err = $@) {
$errmsg ? die "$errmsg: $err" : die $err;
}
}
sub write_usb_config {
my ($cfg) = @_;
cfs_write_file($FILENAME, $cfg);
}
sub find_on_current_node {
my ($id) = @_;
my $cfg = config();
my $node = PVE::INotify::nodename();
return get_node_mapping($cfg, $id, $node);
}
sub get_node_mapping {
my ($cfg, $id, $nodename) = @_;
return undef if !defined($cfg->{ids}->{$id});
my $res = [];
for my $map ($cfg->{ids}->{$id}->{map}->@*) {
my $entry = eval { parse_property_string($map_fmt, $map) };
warn $@ if $@;
if ($entry && $entry->{node} eq $nodename) {
push $res->@*, $entry;
}
}
return $res;
}
PVE::Mapping::USB->register();
PVE::Mapping::USB->init();
1;