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.csa...@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 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel