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 581D37CD57 for ; Tue, 19 Jul 2022 13:47:26 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id DB5E729092 for ; Tue, 19 Jul 2022 13:46:54 +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, 19 Jul 2022 13:46:41 +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 8F4F84286D for ; Tue, 19 Jul 2022 13:46:41 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Date: Tue, 19 Jul 2022 13:46:20 +0200 Message-Id: <20220719114639.3035048-5-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20220719114639.3035048-1-d.csapak@proxmox.com> References: <20220719114639.3035048-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.092 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. [ldap.pm, jsonschema.pm, hardwaremap.pm, daemon.pm, format.pm, inotify.pm, exception.pm] Subject: [pve-devel] [PATCH common 1/1] 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, 19 Jul 2022 11:47:26 -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 => "" } 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 --- 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