[pve-devel] [PATCH guest-common v4 1/1] add PCI/USB Resource configs
Dominik Csapak
d.csapak at proxmox.com
Thu May 25 12:40:43 CEST 2023
On 5/25/23 12:17, Dominik Csapak wrote:
> 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:
>
> ---
> some-pci-device
> description some device
> map node=node1,path=0000:00:01.0,id=1234:1234,iommugroup=4
> map node=node2,path=0000:01:01.0,id=1234:1234,iommugroup=5
> map node=node3,path=0000:02:01.0,id=1234:1234,iommugroup=6
> ---
>
> some special notes:
> * mdev is a per config entry, since it does not make sense to mix mdev
> and non-mdev devices
> * there can be multiple pci paths given per node mapping, this is
> intended to have a different semantic than in the qemu config, namely
> it will select the first available instead of passing all through as a
> multifunction device
just noticed that commit message is slightly outdated, a single mapping
has the same semantic as in the qemu config, and when you want multiple mappings
per node there must be multiple entries
>
> Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
> ---
> src/Makefile | 3 +
> src/PVE/Resource/PCI.pm | 226 ++++++++++++++++++++++++++++++++++++++++
> src/PVE/Resource/USB.pm | 183 ++++++++++++++++++++++++++++++++
> 3 files changed, 412 insertions(+)
> create mode 100644 src/PVE/Resource/PCI.pm
> create mode 100644 src/PVE/Resource/USB.pm
>
> diff --git a/src/Makefile b/src/Makefile
> index 57a360b..d92b441 100644
> --- a/src/Makefile
> +++ b/src/Makefile
> @@ -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/Resource
> + install -m 0644 PVE/Resource/PCI.pm ${PERL5DIR}/PVE/Resource/
> + install -m 0644 PVE/Resource/USB.pm ${PERL5DIR}/PVE/Resource/
> 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/
> diff --git a/src/PVE/Resource/PCI.pm b/src/PVE/Resource/PCI.pm
> new file mode 100644
> index 0000000..d4d9c44
> --- /dev/null
> +++ b/src/PVE/Resource/PCI.pm
> @@ -0,0 +1,226 @@
> +package PVE::Resource::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 = 'resource/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 resource.",
> + 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::Resource::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::Resource::PCI->register();
> +PVE::Resource::PCI->init();
> +
> +1;
> diff --git a/src/PVE/Resource/USB.pm b/src/PVE/Resource/USB.pm
> new file mode 100644
> index 0000000..05ea789
> --- /dev/null
> +++ b/src/PVE/Resource/USB.pm
> @@ -0,0 +1,183 @@
> +package PVE::Resource::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 = 'resource/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 resource.",
> + 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::Resource::USB->register();
> +PVE::Resource::USB->init();
> +
> +1;
More information about the pve-devel
mailing list