[pve-devel] [PATCH common v3 2/3] add PVE/HardwareMap
Dominik Csapak
d.csapak at proxmox.com
Tue Sep 20 14:50:14 CEST 2022
this adds functionality for the hardwaremap config (as json)
the format of the config is like this:
{
usb => {
name => {
nodename1 => { /* mapping object */ },
nodename2 => { /* mapping object */ }
}
},
pci => {
/* same as above */
},
digest => "<DIGEST-STRING>"
}
a single mapping object contains some info about the device, e.g.
for pci the (sub)vendor, sub(device), the mdev capability, path, etc.
for pci multifunction devices (e.g. 01:02 instead of 01:02.0), we use
the values of the first function to identify it.
note that for multifunction devices, we require 'mdev' to be undef
regardless what the first function is capable of, since
we cannot use mediated devices with multifunction devices anyway.
it also adds some helpers for the api schema & asserting that the
device mappings are valid (by checking the saved properties
against the ones found on the current available devices)
we use a single cluster wide json here, because section config is too
limited to properly represent the data we need and easily access the
config of all nodes when we need it (e.g. in api calls regarding
migration and configuration)
Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
src/Makefile | 1 +
src/PVE/HardwareMap.pm | 367 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 368 insertions(+)
create mode 100644 src/PVE/HardwareMap.pm
diff --git a/src/Makefile b/src/Makefile
index 13de6c6..8527704 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -17,6 +17,7 @@ LIB_SOURCES = \
Daemon.pm \
Exception.pm \
Format.pm \
+ HardwareMap.pm \
INotify.pm \
JSONSchema.pm \
LDAP.pm \
diff --git a/src/PVE/HardwareMap.pm b/src/PVE/HardwareMap.pm
new file mode 100644
index 0000000..31841b4
--- /dev/null
+++ b/src/PVE/HardwareMap.pm
@@ -0,0 +1,367 @@
+package PVE::HardwareMap;
+
+use strict;
+use warnings;
+
+use Digest::SHA;
+use JSON;
+use Storable qw(dclone);
+
+use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
+use PVE::INotify;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::SysFSTools;
+
+use base qw(Exporter);
+
+our @EXPORT_OK = qw(find_device_on_current_node);
+
+my $FILENAME = "nodes/hardware-map.conf";
+cfs_register_file($FILENAME, \&read_hardware_map, \&write_hardware_map);
+
+my $PCI_RE = "[a-f0-9]{4,}:[a-f0-9]{2}:[a-f0-9]{2}(?:\.[a-f0-9])?";
+
+# a mapping format per type
+my $format = {
+ usb => {
+ vendor => {
+ description => "The vendor ID",
+ type => 'string',
+ pattern => qr/^(:?0x)?[0-9A-Fa-f]{4}$/,
+ },
+ device => {
+ description => "The device ID",
+ type => 'string',
+ pattern => qr/^(:?0x)?[0-9A-Fa-f]{4}$/,
+ },
+ 'subsystem-vendor' => {
+ description => "The subsystem vendor ID",
+ type => 'string',
+ pattern => qr/^(:?0x)?[0-9A-Fa-f]{4}$/,
+ optional => 1,
+ },
+ 'subsystem-device' => {
+ description => "The subsystem device ID",
+ type => 'string',
+ pattern => qr/^(:?0x)?[0-9A-Fa-f]{4}$/,
+ optional => 1,
+ },
+ path => {
+ description => "The path to the usb device.",
+ type => 'string',
+ optional => 1,
+ pattern => qr/^(\d+)\-(\d+(\.\d+)*)$/,
+ },
+ comment => {
+ description => "Description.",
+ type => 'string',
+ optional => 1,
+ maxLength => 4096,
+ },
+ },
+ pci => {
+ vendor => {
+ description => "The vendor ID",
+ type => 'string',
+ pattern => qr/^(:?0x)?[0-9A-Fa-f]{4}$/,
+ },
+ device => {
+ description => "The device ID",
+ type => 'string',
+ pattern => qr/^(:?0x)?[0-9A-Fa-f]{4}$/,
+ },
+ 'subsystem-vendor' => {
+ description => "The subsystem vendor ID",
+ type => 'string',
+ pattern => qr/^(:?0x)?[0-9A-Fa-f]{4}$/,
+ optional => 1,
+ },
+ 'subsystem-device' => {
+ description => "The subsystem device ID",
+ type => 'string',
+ pattern => qr/^(:?0x)?[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.",
+ type => 'string',
+ pattern => "${PCI_RE}",
+ },
+ mdev => {
+ description => "The Device supports mediated devices.",
+ type => 'boolean',
+ optional => 1,
+ default => 0,
+ },
+ iommugroup => {
+ type => 'integer',
+ description => "The IOMMU group in which the device is in.",
+ optional => 1,
+ },
+ comment => {
+ description => "Description.",
+ type => 'string',
+ optional => 1,
+ maxLength => 4096,
+ },
+ },
+};
+
+my $name_format = {
+ description => "The custom name for the device",
+ type => 'string',
+ format => 'pve-configid',
+};
+
+sub find_device_on_current_node {
+ my ($type, $id) = @_;
+
+ my $cfg = config();
+ my $node = PVE::INotify::nodename();
+
+ return undef if !defined($cfg->{$type}->{$id}) || !defined($cfg->{$type}->{$id}->{$node});
+ return $cfg->{$type}->{$id}->{$node};
+}
+
+sub config {
+ return cfs_read_file($FILENAME);
+}
+
+sub lock_config {
+ my ($code, $errmsg) = @_;
+
+ cfs_lock_file($FILENAME, undef, $code);
+ if (my $err = $@) {
+ $errmsg ? die "$errmsg: $err" : die $err;
+ }
+}
+
+sub write_config {
+ my ($cfg) = @_;
+
+ cfs_write_file($FILENAME, $cfg);
+}
+
+sub check_prop {
+ my ($schema, $key, $value) = @_;
+ my $errors = {};
+ PVE::JSONSchema::check_prop($value, $schema, '', $errors);
+ if (scalar(keys %$errors)) {
+ die "$errors->{$key}\n" if $errors->{$key};
+ die "$errors->{_root}\n" if $errors->{_root};
+ die "unknown error\n";
+ }
+}
+
+sub check_config {
+ my ($cfg) = @_;
+
+ for my $type (keys %$format) {
+ my $type_cfg = $cfg->{$type};
+ my $type_format = $format->{$type};
+
+ for my $name (keys %$type_cfg) {
+ check_prop($name_format, 'name', $name);
+
+ for my $node (keys $type_cfg->{$name}->%*) {
+ check_prop(get_standard_option('pve-node'), 'node', $node);
+ my $entry = $type_cfg->{$name}->{$node};
+
+ # check required props
+ for my $prop (keys %$type_format) {
+ next if $type_format->{$prop}->{optional};
+ die "missing property '$prop' for $type entry '$name'\n"
+ if !defined($entry->{$prop});
+ }
+
+ for my $prop (keys %$entry) {
+ check_prop($type_format->{$prop}, $prop, $entry->{$prop});
+ }
+ }
+ }
+ }
+}
+
+sub read_hardware_map {
+ my ($filename, $raw) = @_;
+
+ $raw //= '';
+ my $digest = Digest::SHA::sha1_hex($raw);
+
+ if ($raw eq '') {
+ return {
+ digest => $digest,
+ };
+ }
+
+ my $cfg = from_json($raw);
+ check_config($cfg);
+ $cfg->{digest} = $digest;
+
+ return $cfg;
+}
+
+sub write_hardware_map {
+ my ($filename, $cfg) = @_;
+
+ check_config($cfg);
+
+ return to_json($cfg);
+}
+
+my $pci_valid = sub {
+ my ($cfg) = @_;
+
+ 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 = {
+ vendor => $info->{vendor},
+ device => $info->{device},
+ 'subsystem-vendor' => $info->{'subsystem_vendor'},
+ 'subsystem-device' => $info->{'subsystem_device'},
+ mdev => $multifunction ? undef : $info->{mdev}, # don't allow mdev for multifunction
+ iommugroup => $info->{iommugroup},
+ };
+
+ for my $prop (sort keys %$correct_props) {
+ 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//;
+ my $configured_prop = $cfg->{$prop};
+ $configured_prop =~ s/^0x//;
+
+ die "'$prop' does not match for '$cfg->{name}' ($correct_prop != $configured_prop)\n"
+ if $correct_prop ne $configured_prop;
+ }
+
+ return 1;
+};
+
+my $usb_valid = sub {
+ my ($cfg) = @_;
+
+ my $name = $cfg->{name};
+ my $vendor = $cfg->{vendor};
+ my $device = $cfg->{device};
+
+ 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);
+
+ die "'vendor' does not match for '$name'\n"
+ if $info->{vendid} ne $cfg->{vendor};
+ die "'device' does not match for '$name'\n"
+ if $info->{prodid} ne $cfg->{device};
+ } else {
+ for my $dev (@$usb_list) {
+ next if $dev->{vendid} ne $vendor;
+ next if $dev->{prodid} ne $device;
+ $info = $dev;
+ }
+ die "usb device '$vendor:$device' not found\n" if !defined($info);
+ }
+
+ return 1;
+};
+
+sub assert_device_valid {
+ my ($type, $cfg) = @_;
+
+ if ($type eq 'usb') {
+ return $usb_valid->($cfg);
+ } elsif ($type eq 'pci') {
+ return $pci_valid->($cfg);
+ }
+
+ die "invalid type $type\n";
+}
+
+sub createSchema {
+ my ($type) = @_;
+
+ my $schema = {};
+
+ $schema->{name} = $name_format;
+ $schema->{node} = get_standard_option('pve-node');
+
+ for my $opt (sort keys $format->{$type}->%*) {
+ $schema->{$opt} = $format->{$type}->{$opt};
+ }
+
+ return {
+ additionalProperties => 0,
+ properties => $schema,
+ };
+
+}
+
+sub updateSchema {
+ my ($type) = @_;
+
+ my $schema = {};
+
+ $schema->{name} = $name_format;
+ $schema->{node} = get_standard_option('pve-node');
+
+ my $deletable = [];
+
+ for my $opt (sort keys $format->{$type}->%*) {
+ $schema->{$opt} = dclone($format->{$type}->{$opt});
+ $schema->{$opt}->{optional} = 1;
+ if ($format->{$type}->{$opt}->{optional}) {
+ push @$deletable, $opt;
+ }
+ }
+
+ my $deletable_pattern = join('|', @$deletable);
+
+ $schema->{delete} = {
+ type => 'string', format => 'pve-configid-list',
+ description => "A list of settings you want to delete.",
+ maxLength => 4096,
+ optional => 1,
+ };
+
+ $schema->{digest} = get_standard_option('pve-config-digest');
+
+ return {
+ additionalProperties => 0,
+ properties => $schema,
+ };
+}
+
+sub options {
+ my ($type) = @_;
+
+ my $opts = {};
+
+ for my $opt (sort keys $format->{$type}->%*) {
+ $opts->{$opt}->{optional} = $format->{$type}->{$opt}->{optional};
+ }
+
+ return $opts;
+}
+
+1;
--
2.30.2
More information about the pve-devel
mailing list