From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id AE76493EC5 for ; Tue, 20 Sep 2022 14:53:46 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id E9E881CA5F for ; Tue, 20 Sep 2022 14:53:44 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Tue, 20 Sep 2022 14:53:38 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 06EF64423D for ; Tue, 20 Sep 2022 14:50:47 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Date: Tue, 20 Sep 2022 14:50:14 +0200 Message-Id: <20220920125041.3636561-9-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20220920125041.3636561-1-d.csapak@proxmox.com> References: <20220920125041.3636561-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.078 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [inotify.pm, exception.pm, format.pm, hardwaremap.pm, jsonschema.pm, daemon.pm, ldap.pm] Subject: [pve-devel] [PATCH common v3 2/3] add PVE/HardwareMap X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 20 Sep 2022 12:53:46 -0000 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 => "" } 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 --- 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