[pve-devel] [PATCH common 1/1] add PVE/HardwareMap
Dominik Csapak
d.csapak at proxmox.com
Tue Jul 19 13:46:20 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>"
}
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)
Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
src/Makefile | 1 +
src/PVE/HardwareMap.pm | 363 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 364 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..1b94abc
--- /dev/null
+++ b/src/PVE/HardwareMap.pm
@@ -0,0 +1,363 @@
+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);
+
+# 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 attrubes of the first device.",
+ type => 'string',
+ pattern => "(?:[a-f0-9]{4,}:)?[a-f0-9]{2}:[a-f0-9]{2}(?:\.[a-f0-9])?",
+ },
+ 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) = @_;
+
+ my $digest = Digest::SHA::sha1_hex($raw);
+
+ if (!defined($raw) || $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 $path = $cfg->{path} // '';
+
+ if ($path !~ m/\.[a-f0-9]/i) {
+ # whole device, add .0 (must exist)
+ $path = "$path.0";
+ }
+
+ 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 => $info->{mdev},
+ 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