From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <d.csapak@proxmox.com>
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 <pve-devel@lists.proxmox.com>; 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 <pve-devel@lists.proxmox.com>; 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 <pve-devel@lists.proxmox.com>; 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 <pve-devel@lists.proxmox.com>; Tue, 19 Jul 2022 13:46:41 +0200 (CEST)
From: Dominik Csapak <d.csapak@proxmox.com>
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 <pve-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pve-devel/>
List-Post: <mailto:pve-devel@lists.proxmox.com>
List-Help: <mailto:pve-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=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 => "<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@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