[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