all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH guest-common v4 1/1] add PCI/USB Resource configs
Date: Thu, 25 May 2023 12:17:45 +0200	[thread overview]
Message-ID: <20230525101753.2078811-4-d.csapak@proxmox.com> (raw)
In-Reply-To: <20230525101753.2078811-1-d.csapak@proxmox.com>

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

Signed-off-by: Dominik Csapak <d.csapak@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;
-- 
2.30.2





  parent reply	other threads:[~2023-05-25 10:17 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-05-25 10:17 [pve-devel] [PATCH cluster/access-control/guest-common/qemu-server/manager v4] cluster mapping backend Dominik Csapak
2023-05-25 10:17 ` [pve-devel] [PATCH cluster v4 1/1] add cfg files for resource mapping Dominik Csapak
2023-06-05  9:11   ` [pve-devel] applied: " Thomas Lamprecht
2023-05-25 10:17 ` [pve-devel] [PATCH access-control v4 1/1] add privileges and paths for cluster " Dominik Csapak
2023-05-25 10:17 ` Dominik Csapak [this message]
2023-05-25 10:40   ` [pve-devel] [PATCH guest-common v4 1/1] add PCI/USB Resource configs Dominik Csapak
2023-05-25 10:17 ` [pve-devel] [PATCH qemu-server v4 1/6] enable cluster mapped USB devices for guests Dominik Csapak
2023-05-25 10:17 ` [pve-devel] [PATCH qemu-server v4 2/6] enable cluster mapped PCI " Dominik Csapak
2023-05-25 10:17 ` [pve-devel] [PATCH qemu-server v4 3/6] check_local_resources: extend for mapped resources Dominik Csapak
2023-05-25 10:17 ` [pve-devel] [PATCH qemu-server v4 4/6] api: migrate preconditions: use new check_local_resources info Dominik Csapak
2023-05-25 10:17 ` [pve-devel] [PATCH qemu-server v4 5/6] migration: check for mapped resources Dominik Csapak
2023-05-25 10:17 ` [pve-devel] [PATCH qemu-server v4 6/6] add test for mapped pci devices Dominik Csapak
2023-05-25 10:17 ` [pve-devel] [PATCH manager v4 1/2] pvesh: fix parameters for proxyto_callback Dominik Csapak
2023-05-25 10:17 ` [pve-devel] [PATCH manager v4 2/2] api: add resource map api endpoints for PCI and USB Dominik Csapak
2023-05-26 16:09 ` [pve-devel] [PATCH cluster/access-control/guest-common/qemu-server/manager v4] cluster mapping backend DERUMIER, Alexandre

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20230525101753.2078811-4-d.csapak@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal