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 AA9DD8B602 for ; Thu, 25 Aug 2022 11:25:05 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 317E623321 for ; Thu, 25 Aug 2022 11:25:04 +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 ; Thu, 25 Aug 2022 11:24:49 +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 9D42343FF2 for ; Thu, 25 Aug 2022 11:24:42 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Date: Thu, 25 Aug 2022 11:24:13 +0200 Message-Id: <20220825092440.1810328-5-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20220825092440.1810328-1-d.csapak@proxmox.com> References: <20220825092440.1810328-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.094 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 T_SCC_BODY_TEXT_LINE -0.01 - URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [ldap.pm, daemon.pm, format.pm, hardwaremap.pm, inotify.pm, exception.pm, jsonschema.pm] Subject: [pve-devel] [PATCH common v2 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: Thu, 25 Aug 2022 09:25:05 -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. 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 | 364 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 365 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..7a90220 --- /dev/null +++ b/src/PVE/HardwareMap.pm @@ -0,0 +1,364 @@ +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) = @_; + + 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) = @_; + + 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