From 9355f5c2b984df607958806e93b66d4b1eb36a40 Mon Sep 17 00:00:00 2001 From: Oliver Gorwits Date: Mon, 29 May 2023 21:32:07 +0100 Subject: [PATCH] Refactored ACL support with multi-object compare Squashed commit of the following: commit 4081e22202693bd7c4ea00e95daad8e628c6fd5a Author: Oliver Gorwits Date: Mon May 29 21:02:07 2023 +0100 large rename of check_acl* to acl_matches* commit 3cfa284ddd24d68765c255578cc5c184afbdcd83 Author: Oliver Gorwits Date: Fri May 19 20:39:03 2023 +0100 update permission doc commit 8c7bb93cc5e9fafb770f98f446e45cbd94b14894 Author: Oliver Gorwits Date: Wed May 17 21:50:07 2023 +0100 migrate most check_acl_only to acl_matches_only commit c47f699f2a22f08f2f3e093ed0f24c891e6f9a82 Author: Oliver Gorwits Date: Wed May 17 21:39:19 2023 +0100 rename check_acl* to be acl_matches* commit a884a22c3ab1f3262118c3a47ed8e25b0b0a7336 Author: Oliver Gorwits Date: Sun May 14 16:50:42 2023 +0100 update macsuck_no_deviceports to use acl_matches commit 8c256af728721329b64d071fa529dfc844073ac6 Author: Oliver Gorwits Date: Sun May 7 22:54:33 2023 +0100 update hide_deviceports to use acl_matches multi @things commit cd5d9978aba1da459be4fed4500f395df13f7784 Author: Oliver Gorwits Date: Sun May 7 22:53:38 2023 +0100 check_acl fix to allow all @things to offer a property before fallback to missing as empty string commit 1a3ab9a7646e9f994f03126d45fc36e9e5a13ed5 Author: Oliver Gorwits Date: Tue May 2 15:31:17 2023 +0100 add ignore_deviceports to portproperties discover; improve comments commit 51385ce89458dc939587dae902fda431719c22c9 Merge: b97c07d2 3f8ffe78 Author: Oliver Gorwits Date: Tue May 2 15:21:48 2023 +0100 Merge branch 'master' into og-acl_multidict commit b97c07d237d750c1d9eb3095d8ff3908512eac2a Author: Oliver Gorwits Date: Sat Mar 25 14:37:53 2023 +0000 add support for arrayref of items, and unblessed hash, to check_acl --- Build.PL | 1 - bin/netdisco-rancid-export | 4 +- lib/App/Netdisco/DB/Result/DeviceIp.pm | 35 --- lib/App/Netdisco/Transport/SNMP.pm | 14 +- lib/App/Netdisco/Util/Device.pm | 24 +- lib/App/Netdisco/Util/FastResolver.pm | 4 +- lib/App/Netdisco/Util/Node.pm | 6 +- lib/App/Netdisco/Util/Permission.pm | 268 ++++++++++++------ lib/App/Netdisco/Util/Port.pm | 6 +- .../Netdisco/Web/Plugin/Device/Neighbors.pm | 6 +- lib/App/Netdisco/Web/Plugin/Device/Ports.pm | 30 +- lib/App/Netdisco/Worker/Plugin.pm | 6 +- .../Netdisco/Worker/Plugin/Arpnip/Hooks.pm | 6 +- .../Netdisco/Worker/Plugin/Arpnip/Subnets.pm | 4 +- .../Netdisco/Worker/Plugin/Delete/Hooks.pm | 6 +- .../Worker/Plugin/Discover/CanonicalIP.pm | 50 ++-- .../Netdisco/Worker/Plugin/Discover/Hooks.pm | 6 +- .../Worker/Plugin/Discover/Neighbors.pm | 4 +- .../Plugin/Discover/Neighbors/Routed.pm | 6 +- .../Worker/Plugin/Discover/PortProperties.pm | 26 +- .../Worker/Plugin/Discover/Properties.pm | 56 ++-- .../Worker/Plugin/Discover/WithNodes.pm | 10 +- .../Netdisco/Worker/Plugin/Macsuck/Hooks.pm | 6 +- .../Netdisco/Worker/Plugin/Macsuck/Nodes.pm | 17 +- .../Netdisco/Worker/Plugin/MakeRancidConf.pm | 12 +- lib/App/Netdisco/Worker/Runner.pm | 6 +- xt/20-checkacl.t | 179 ++++++++---- 27 files changed, 463 insertions(+), 335 deletions(-) diff --git a/Build.PL b/Build.PL index a99237af..a3e9b365 100644 --- a/Build.PL +++ b/Build.PL @@ -90,7 +90,6 @@ Module::Build->new( 'SNMP::Info' => '3.92', 'SQL::Abstract' => '1.85', 'SQL::Translator' => '0.11024', - 'Sub::Install' => '0', 'Sub::Util' => '1.40', 'Template' => '2.24', 'Template::AutoFilter' => '0', diff --git a/bin/netdisco-rancid-export b/bin/netdisco-rancid-export index 8febea4e..ea38c66e 100755 --- a/bin/netdisco-rancid-export +++ b/bin/netdisco-rancid-export @@ -40,7 +40,7 @@ use App::Netdisco; use Dancer ':script'; use Dancer::Plugin::DBIC 'schema'; -use App::Netdisco::Util::Permission ':all'; +use App::Netdisco::Util::Permission 'acl_matches'; # silent exit unless explicitly requested exit(0) unless setting('use_legacy_rancidexport'); @@ -76,7 +76,7 @@ foreach my $d (@devices) { my $old = $d->get_column( 'old' ); my $devgroup = 'other'; foreach my $g (keys %$groups) { - if (check_acl_only( $d, $groups->{$g} )) { + if (acl_matches( $d, $groups->{$g} )) { $devgroup = $g; last; } diff --git a/lib/App/Netdisco/DB/Result/DeviceIp.pm b/lib/App/Netdisco/DB/Result/DeviceIp.pm index bef5e505..1487731e 100644 --- a/lib/App/Netdisco/DB/Result/DeviceIp.pm +++ b/lib/App/Netdisco/DB/Result/DeviceIp.pm @@ -6,7 +6,6 @@ use strict; use warnings; use base 'App::Netdisco::DB::Result'; -use Sub::Install; __PACKAGE__->table("device_ip"); __PACKAGE__->add_columns( @@ -52,38 +51,4 @@ routed port or virtual interface). __PACKAGE__->belongs_to( device_port => 'App::Netdisco::DB::Result::DevicePort', { 'foreign.port' => 'self.port', 'foreign.ip' => 'self.ip' } ); -=head2 device_port fields - -All C fields are mapped to accessors on this object. - -=cut - -foreach my $field (qw/ - descr - up - up_admin - type - duplex - duplex_admin - speed - speed_admin - name - mac - mtu - stp - remote_ip - remote_port - remote_type - remote_id - vlan - pvid - lastchange - /) { - - Sub::Install::install_sub({ - code => sub { return eval { (shift)->device_port->$field } }, - as => $field, - }); -} - 1; diff --git a/lib/App/Netdisco/Transport/SNMP.pm b/lib/App/Netdisco/Transport/SNMP.pm index ead74a7a..20217883 100644 --- a/lib/App/Netdisco/Transport/SNMP.pm +++ b/lib/App/Netdisco/Transport/SNMP.pm @@ -5,7 +5,7 @@ use Dancer::Plugin::DBIC 'schema'; use App::Netdisco::Util::SNMP 'get_communities'; use App::Netdisco::Util::Device 'get_device'; -use App::Netdisco::Util::Permission ':all'; +use App::Netdisco::Util::Permission 'acl_matches'; use SNMP::Info; use Try::Tiny; @@ -97,7 +97,7 @@ sub test_connection { # avoid renumbering to localhost loopbacks return undef if $addr->addr eq '0.0.0.0' - or check_acl_no($addr->addr, 'group:__LOOPBACK_ADDRESSES__'); + or acl_matches($addr->addr, 'group:__LOOPBACK_ADDRESSES__'); my $device = schema(vars->{'tenant'})->resultset('Device') ->new_result({ ip => $addr->addr }) or return undef; @@ -154,11 +154,11 @@ sub _snmp_connect_generic { # an override for RemotePort ($snmp_args{RemotePort}) = - (pairkeys pairfirst { check_acl_no($device, $b) } + (pairkeys pairfirst { acl_matches($device, $b) } %{setting('snmp_remoteport') || {}}) || 161; # an override for bulkwalk - $snmp_args{BulkWalk} = 0 if check_acl_no($device, 'bulkwalk_no'); + $snmp_args{BulkWalk} = 0 if acl_matches($device, 'bulkwalk_no'); # further protect against buggy Net-SNMP, and disable bulkwalk if ($snmp_args{BulkWalk} @@ -197,9 +197,9 @@ sub _snmp_connect_generic { # which SNMP versions to try and in what order my @versions = - ( check_acl_no($device->ip, 'snmpforce_v3') ? (3) - : check_acl_no($device->ip, 'snmpforce_v2') ? (2) - : check_acl_no($device->ip, 'snmpforce_v1') ? (1) + ( acl_matches($device->ip, 'snmpforce_v3') ? (3) + : acl_matches($device->ip, 'snmpforce_v2') ? (2) + : acl_matches($device->ip, 'snmpforce_v1') ? (1) : (reverse (1 .. (setting('snmpver') || 3))) ); # use existing or new device class diff --git a/lib/App/Netdisco/Util/Device.pm b/lib/App/Netdisco/Util/Device.pm index 3cb3c08b..61280290 100644 --- a/lib/App/Netdisco/Util/Device.pm +++ b/lib/App/Netdisco/Util/Device.pm @@ -2,7 +2,7 @@ package App::Netdisco::Util::Device; use Dancer qw/:syntax :script/; use Dancer::Plugin::DBIC 'schema'; -use App::Netdisco::Util::Permission qw/check_acl_no check_acl_only/; +use App::Netdisco::Util::Permission qw/acl_matches acl_matches_only/; use File::Spec::Functions qw(catdir catfile); use File::Path 'make_path'; @@ -184,10 +184,10 @@ sub is_discoverable { if (match_to_setting($remote_type, 'discover_no_type')); return _bail_msg("is_discoverable: $device matched discover_no") - if check_acl_no($device, 'discover_no'); + if acl_matches($device, 'discover_no'); return _bail_msg("is_discoverable: $device failed to match discover_only") - unless check_acl_only($device, 'discover_only'); + unless acl_matches_only($device, 'discover_only'); return 1; } @@ -236,14 +236,14 @@ sub is_arpnipable { return _bail_msg("is_arpnipable: $device has no layer 3 capability") if ($device->in_storage() and not ($device->has_layer(3) - or check_acl_no($device, 'force_arpnip') - or check_acl_no($device, 'ignore_layers'))); + or acl_matches($device, 'force_arpnip') + or acl_matches($device, 'ignore_layers'))); return _bail_msg("is_arpnipable: $device matched arpnip_no") - if check_acl_no($device, 'arpnip_no'); + if acl_matches($device, 'arpnip_no'); return _bail_msg("is_arpnipable: $device failed to match arpnip_only") - unless check_acl_only($device, 'arpnip_only'); + unless acl_matches_only($device, 'arpnip_only'); return 1; } @@ -292,17 +292,17 @@ sub is_macsuckable { return _bail_msg("is_macsuckable: $device has no layer 2 capability") if ($device->in_storage() and not ($device->has_layer(2) - or check_acl_no($device, 'force_macsuck') - or check_acl_no($device, 'ignore_layers'))); + or acl_matches($device, 'force_macsuck') + or acl_matches($device, 'ignore_layers'))); return _bail_msg("is_macsuckable: $device matched macsuck_no") - if check_acl_no($device, 'macsuck_no'); + if acl_matches($device, 'macsuck_no'); return _bail_msg("is_macsuckable: $device matched macsuck_unsupported") - if check_acl_no($device, 'macsuck_unsupported'); + if acl_matches($device, 'macsuck_unsupported'); return _bail_msg("is_macsuckable: $device failed to match macsuck_only") - unless check_acl_only($device, 'macsuck_only'); + unless acl_matches_only($device, 'macsuck_only'); return 1; } diff --git a/lib/App/Netdisco/Util/FastResolver.pm b/lib/App/Netdisco/Util/FastResolver.pm index 842ff4dd..7b1ae9bb 100644 --- a/lib/App/Netdisco/Util/FastResolver.pm +++ b/lib/App/Netdisco/Util/FastResolver.pm @@ -5,7 +5,7 @@ use warnings; use Dancer ':script'; use AnyEvent::DNS; -use App::Netdisco::Util::Permission 'check_acl_no'; +use App::Netdisco::Util::Permission 'acl_matches'; use base 'Exporter'; our @EXPORT = (); @@ -58,7 +58,7 @@ sub hostnames_resolve_async { IP: foreach my $hash_ref (@$ips) { my $ip = $hash_ref->{'ip'} || $hash_ref->{'alias'} || $hash_ref->{'device'}; - next IP if check_acl_no($ip, $skip); + next IP if acl_matches($ip, $skip); # check /etc/hosts file and short-circuit if found foreach my $name (reverse sort keys %$ETCHOSTS) { diff --git a/lib/App/Netdisco/Util/Node.pm b/lib/App/Netdisco/Util/Node.pm index 717d8efb..af0390b9 100644 --- a/lib/App/Netdisco/Util/Node.pm +++ b/lib/App/Netdisco/Util/Node.pm @@ -4,7 +4,7 @@ use Dancer qw/:syntax :script/; use Dancer::Plugin::DBIC 'schema'; use NetAddr::MAC; -use App::Netdisco::Util::Permission qw/check_acl_no check_acl_only/; +use App::Netdisco::Util::Permission qw/acl_matches acl_matches_only/; use base 'Exporter'; our @EXPORT = (); @@ -138,9 +138,9 @@ Returns false if the host is not permitted to nbtstat the target node. sub is_nbtstatable { my $ip = shift; - return if check_acl_no($ip, 'nbtstat_no'); + return if acl_matches($ip, 'nbtstat_no'); - return unless check_acl_only($ip, 'nbtstat_only'); + return unless acl_matches_only($ip, 'nbtstat_only'); return 1; } diff --git a/lib/App/Netdisco/Util/Permission.pm b/lib/App/Netdisco/Util/Permission.pm index f66d170d..94b6117d 100644 --- a/lib/App/Netdisco/Util/Permission.pm +++ b/lib/App/Netdisco/Util/Permission.pm @@ -10,7 +10,7 @@ use App::Netdisco::Util::DNS 'hostname_from_ip'; use base 'Exporter'; our @EXPORT = (); -our @EXPORT_OK = qw/check_acl check_acl_no check_acl_only/; +our @EXPORT_OK = qw/acl_matches acl_matches_only/; our %EXPORT_TAGS = (all => \@EXPORT_OK); =head1 NAME @@ -26,11 +26,17 @@ subroutines. =head1 EXPORT_OK -=head2 check_acl_no( $ip | $instance, $setting_name | $acl_entry | \@acl ) +=head2 acl_matches( $ip | $object | \%hash | \@item_list, $setting_name | $acl_entry | \@acl ) -Given an IP address or object instance, returns true if the configuration -setting C<$setting_name> matches, else returns false. If the content of the -setting is undefined or empty, then C also returns false. +Given an IP address, object instance, or hash, returns true if the +configuration setting C<$setting_name> matches, else returns false. + +Usage of this function is strongly advised to be of the form: + + QUIT/SKIP IF acl_matches + +The function fails safe, so if the content of the setting or ACL is undefined +or an empty string, then C also returns true. If C<$setting_name> is a valid setting, then it will be resolved to the access control list, else we assume you passed an ACL entry or ACL. @@ -40,19 +46,37 @@ for details of what C<$acl> may contain. =cut -sub check_acl_no { +sub acl_matches { my ($thing, $setting_name) = @_; - return 1 unless $thing and $setting_name; + # fail-safe so undef config should return true + return true unless $thing and $setting_name; my $config = (exists config->{"$setting_name"} ? setting($setting_name) : $setting_name); return check_acl($thing, $config); } -=head2 check_acl_only( $ip | $instance, $setting_name | $acl_entry | \@acl ) +=head2 check_acl_no( $ip | $object | \%hash | \@item_list, $setting_name | $acl_entry | \@acl ) -Given an IP address or object instance, returns true if the configuration -setting C<$setting_name> matches, else returns false. If the content of the -setting is undefined or empty, then C also returns true. +This is an alias for L. + +=cut + +sub check_acl_no { goto &acl_matches } + +=head2 acl_matches_only( $ip | $object | \%hash | \@item_list, $setting_name | $acl_entry | \@acl ) + +Given an IP address, object instance, or hash, returns true if the +configuration setting C<$setting_name> matches, else returns false. + +Usage of this function is strongly advised to be of the form: + + QUIT/SKIP UNLESS acl_matches_only + +The function fails safe, so if the content of the setting or ACL is undefined +or an empty string, then C also returns false. + +Further, if the setting or ACL resolves to a list but the list has no items, +then C returns true (as if there is a successful match). If C<$setting_name> is a valid setting, then it will be resolved to the access control list, else we assume you passed an ACL entry or ACL. @@ -62,25 +86,40 @@ for details of what C<$acl> may contain. =cut -sub check_acl_only { +sub acl_matches_only { my ($thing, $setting_name) = @_; - return 0 unless $thing and $setting_name; - # logic to make an empty config be equivalent to 'any' (i.e. a match) + # fail-safe so undef config should return false + return false unless $thing and $setting_name; my $config = (exists config->{"$setting_name"} ? setting($setting_name) : $setting_name); - return 1 if not $config # undef or empty string + # logic to make an empty config be equivalent to 'any' (i.e. a match) + # empty list check means truth check passes for match or empty list + return true if not $config # undef or empty string or ((ref [] eq ref $config) and not scalar @$config); return check_acl($thing, $config); } -=head2 check_acl( $ip | $instance, $acl_entry | \@acl ) +=head2 check_acl_only( $ip | $object | \%hash | \@item_list, $setting_name | $acl_entry | \@acl ) -Given an IP address or object instance, compares it to the items in C<< \@acl ->> then returns true or false. You can control whether any item must match or -all must match, and items can be negated to invert the match logic. +This is an alias for L. -Accepts instances of classes representing Netdisco Devices, Netdisco Device -IPs, and L family objects. +=cut + +sub check_acl_only { goto &acl_matches_only } + +=head2 check_acl( $ip | $object | \%hash | \@item_list, $acl_entry | \@acl ) + +Given an IP address, object instance, or hash, compares it to the items in +C<< \@acl >> then returns true or false. You can control whether any item must +match or all must match, and items can be negated to invert the match logic. + +Also accepts an array reference of multiple IP addresses, object instances, +and hashes, and will test against each in turn, for each ACL rule. + +The slots C, C, C, and C are looked for in the +instance or hash and used to compare a bare IP address (so it works with most +Netdisco database classes, and the L class). Any instance or hash +slot can be used as an ACL named property. There are several options for what C<< \@acl >> may contain. See L @@ -89,156 +128,199 @@ for the details. =cut sub check_acl { - my ($thing, $config) = @_; - return 0 unless defined $thing and defined $config; + my ($things, $config) = @_; + return false unless defined $things and defined $config; + return false if ref [] eq ref $things and not scalar @$things; + $things = [$things] if ref [] ne ref $things; - my $real_ip = $thing; - if (blessed $thing) { - $real_ip = ( - $thing->can('alias') ? $thing->alias : ( - $thing->can('ip') ? $thing->ip : ( - $thing->can('addr') ? $thing->addr : $thing ))); + my $real_ip = ''; # valid to be empty + ITEM: foreach my $item (@$things) { + foreach my $slot (qw/alias ip switch addr/) { + if (blessed $item) { + $real_ip = $item->$slot if $item->can($slot) + and eval { $item->$slot }; + } + elsif (ref {} eq ref $item) { + $real_ip = $item->{$slot} if exists $item->{$slot} + and $item->{$slot}; + } + last ITEM if $real_ip; + } + } + ITEM: foreach my $item (@$things) { + last ITEM if $real_ip; + $real_ip = $item if (ref $item eq q{}) and $item; } - return 0 if blessed $real_ip; # class we do not understand - $real_ip ||= ''; # valid to be empty - $config = [$config] if ref q{} eq ref $config; + $config = [$config] if ref $config eq q{}; if (ref [] ne ref $config) { error "error: acl is not a single item or list (cannot compare to '$real_ip')"; - return 0; + return false; } my $all = (scalar grep {$_ eq 'op:and'} @$config); # common case of using plain IP in ACL, so string compare for speed my $find = (scalar grep {not reftype $_ and $_ eq $real_ip} @$config); - return 1 if $real_ip and $find and not $all; + return true if $real_ip and $find and not $all; my $addr = NetAddr::IP::Lite->new($real_ip); my $name = undef; # only look up once, and only if qr// is used my $ropt = { retry => 1, retrans => 1, udp_timeout => 1, tcp_timeout => 2 }; my $qref = ref qr//; - INLIST: foreach (@$config) { - my $item = $_; # must copy so that we can modify safely - next INLIST if !defined $item or $item eq 'op:and'; + RULE: foreach (@$config) { + my $rule = $_; # must copy so that we can modify safely + next RULE if !defined $rule or $rule eq 'op:and'; - if ($qref eq ref $item) { + if ($qref eq ref $rule) { # if no IP addr, cannot match its dns - next INLIST unless $addr; + next RULE unless $addr; $name = ($name || hostname_from_ip($addr->addr, $ropt) || '!!none!!'); - if ($name =~ $item) { - return 1 if not $all; + if ($name =~ $rule) { + return true if not $all; } else { - return 0 if $all; + return false if $all; } - next INLIST; + next RULE; } - my $neg = ($item =~ s/^!//); + my $neg = ($rule =~ s/^!//); - if ($item =~ m/^group:(.+)$/) { + if ($rule =~ m/^group:(.+)$/) { my $group = $1; setting('host_groups')->{$group} ||= []; - if ($neg xor check_acl($thing, setting('host_groups')->{$group})) { - return 1 if not $all; + if ($neg xor check_acl($things, setting('host_groups')->{$group})) { + return true if not $all; } else { - return 0 if $all; + return false if $all; } - next INLIST; + next RULE; } - if ($item =~ m/^([^:]+):([^:]*)$/) { + # prop:val + if ($rule =~ m/^([^:]+):([^:]*)$/) { my $prop = $1; my $match = $2 || ''; + my $found = false; - # if not an object, we can't do much with properties - next INLIST unless blessed $thing; + # property exists, undef is allowed to match empty string + ITEM: foreach my $item (@$things) { + if (blessed $item) { + if ($neg xor ($item->can($prop) and + ((!defined eval { $item->$prop } and $match eq q{}) + or + (defined eval { $item->$prop } and ref $item->$prop eq q{} and $item->$prop =~ m/^$match$/)) )) { + return true if not $all; + $found = true; + last ITEM; + } + } + elsif (ref {} eq ref $item) { + if ($neg xor (exists $item->{$prop} and + ((!defined $item->{$prop} and $match eq q{}) + or + (defined $item->{$prop} and ref $item->{$prop} eq q{} and $item->{$prop} =~ m/^$match$/)) )) { + return true if not $all; + $found = true; + last ITEM; + } + } + } - # prop:val - if ($neg xor ($thing->can($prop) and - defined eval { $thing->$prop } and - ref $thing->$prop eq q{} - and $thing->$prop =~ m/^$match$/) ) { - return 1 if not $all; + # missing property matches empty string + # (which is done in a second pass to allow all @$things to be + # inspected for existing properties) + ITEM: foreach my $item (@$things) { + last ITEM if $found; + + if (blessed $item) { + if ($neg xor ($match eq q{} and ! $item->can($prop))) { + return true if not $all; + $found = true; + last ITEM; + } + } + elsif (ref {} eq ref $item) { + # empty or missing property + if ($neg xor ($match eq q{} and ! exists $item->{$prop})) { + return true if not $all; + $found = true; + last ITEM; + } + } } - # empty or missing property - elsif ($neg xor ($match eq q{} and - (!defined eval { $thing->$prop } or $thing->$prop eq q{})) ) { - return 1 if not $all; - } - else { - return 0 if $all; - } - next INLIST; + + return false if $all; + next RULE; } - if ($item =~ m/[:.]([a-f0-9]+)-([a-f0-9]+)$/i) { + if ($rule =~ m/[:.]([a-f0-9]+)-([a-f0-9]+)$/i) { my $first = $1; my $last = $2; # if no IP addr, cannot match IP range - next INLIST unless $addr; + next RULE unless $addr; - if ($item =~ m/:/) { - next INLIST if $addr->bits != 128 and not $all; + if ($rule =~ m/:/) { + next RULE if $addr->bits != 128 and not $all; $first = hex $first; $last = hex $last; - (my $header = $item) =~ s/:[^:]+$/:/; + (my $header = $rule) =~ s/:[^:]+$/:/; foreach my $part ($first .. $last) { my $ip = NetAddr::IP::Lite->new($header . sprintf('%x',$part) . '/128') or next; if ($neg xor ($ip == $addr)) { - return 1 if not $all; - next INLIST; + return true if not $all; + next RULE; } } - return 0 if (not $neg and $all); - return 1 if ($neg and not $all); + return false if (not $neg and $all); + return true if ($neg and not $all); } else { - next INLIST if $addr->bits != 32 and not $all; + next RULE if $addr->bits != 32 and not $all; - (my $header = $item) =~ s/\.[^.]+$/./; + (my $header = $rule) =~ s/\.[^.]+$/./; foreach my $part ($first .. $last) { my $ip = NetAddr::IP::Lite->new($header . $part . '/32') or next; if ($neg xor ($ip == $addr)) { - return 1 if not $all; - next INLIST; + return true if not $all; + next RULE; } } - return 0 if (not $neg and $all); - return 1 if ($neg and not $all); + return false if (not $neg and $all); + return true if ($neg and not $all); } - next INLIST; + next RULE; } # could be something in error, and IP/host is only option left - next INLIST if ref $item; + next RULE if ref $rule; # if no IP addr, cannot match IP prefix - next INLIST unless $addr; + next RULE unless $addr; - my $ip = NetAddr::IP::Lite->new($item) - or next INLIST; - next INLIST if $ip->bits != $addr->bits and not $all; + my $ip = NetAddr::IP::Lite->new($rule) + or next RULE; + next RULE if $ip->bits != $addr->bits and not $all; if ($neg xor ($ip->contains($addr))) { - return 1 if not $all; + return true if not $all; } else { - return 0 if $all; + return false if $all; } - next INLIST; + next RULE; } - return ($all ? 1 : 0); + return ($all ? true : false); } -1; +true; diff --git a/lib/App/Netdisco/Util/Port.pm b/lib/App/Netdisco/Util/Port.pm index e41a5bd0..9dcf729b 100644 --- a/lib/App/Netdisco/Util/Port.pm +++ b/lib/App/Netdisco/Util/Port.pm @@ -4,7 +4,7 @@ use Dancer qw/:syntax :script/; use Dancer::Plugin::DBIC 'schema'; use App::Netdisco::Util::Device 'get_device'; -use App::Netdisco::Util::Permission qw/check_acl_no check_acl_only/; +use App::Netdisco::Util::Permission qw/acl_matches acl_matches_only/; use base 'Exporter'; our @EXPORT = (); @@ -107,9 +107,9 @@ sub port_reconfig_check { # check for limits on devices return "forbidden: device [$ip] is in denied ACL" - if check_acl_no($ip, 'portctl_no'); + if acl_matches($ip, 'portctl_no'); return "forbidden: device [$ip] is not in permitted ACL" - unless check_acl_only($ip, 'portctl_only'); + unless acl_matches_only($ip, 'portctl_only'); # only permitted to change interface name return "forbidden: not permitted to change port configuration" diff --git a/lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm b/lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm index f926ce77..0c94af4a 100644 --- a/lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm +++ b/lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm @@ -7,7 +7,7 @@ use Dancer::Plugin::Auth::Extensible; use List::Util 'first'; use List::MoreUtils (); -use App::Netdisco::Util::Permission 'check_acl_only'; +use App::Netdisco::Util::Permission 'acl_matches'; use App::Netdisco::Web::Plugin; register_device_tab({ tag => 'netmap', label => 'Neighbors' }); @@ -228,11 +228,11 @@ ajax '/ajax/data/device/netmap' => require_login sub { # if host groups picked then use ACLs to filter my $first_hgrp = - first { check_acl_only($device, setting('host_groups')->{$_}) } @hgrplist; + first { acl_matches($device, setting('host_groups')->{$_}) } @hgrplist; next DEVICE if ((scalar @hgrplist) and (not $first_hgrp)); # now reset first_hgroup to be the group matching the device, if any - $first_hgrp = first { check_acl_only($device, setting('host_groups')->{$_}) } + $first_hgrp = first { acl_matches($device, setting('host_groups')->{$_}) } keys %{ setting('host_group_displaynames') || {} }; ++$logvals{ $device->get_column('log') || 1 }; diff --git a/lib/App/Netdisco/Web/Plugin/Device/Ports.pm b/lib/App/Netdisco/Web/Plugin/Device/Ports.pm index c403c30f..c5c29a93 100644 --- a/lib/App/Netdisco/Web/Plugin/Device/Ports.pm +++ b/lib/App/Netdisco/Web/Plugin/Device/Ports.pm @@ -4,7 +4,7 @@ use Dancer ':syntax'; use Dancer::Plugin::DBIC; use Dancer::Plugin::Auth::Extensible; -use App::Netdisco::Util::Permission 'check_acl_no'; +use App::Netdisco::Util::Permission 'acl_matches'; use App::Netdisco::Util::Port 'port_reconfig_check'; use App::Netdisco::Util::Web (); # for sort_port use App::Netdisco::Web::Plugin; @@ -217,31 +217,33 @@ get '/ajax/content/device/ports' => require_login sub { # filter out hidden ones if (not param('p_include_hidden')) { - my $device_ips = {}; - map { push @{ $device_ips->{$_->port} }, $_ } - $device->device_ips(undef, {prefetch => 'device_port'})->all; + my $port_map = {}; + my %to_hide = (); - map { push @{ $device_ips->{$_->port} }, $_ } - grep { ! exists $device_ips->{$_->port} } + map { push @{ $port_map->{$_->port} }, $_ } + grep { $_->port } @results; + map { push @{ $port_map->{$_->port} }, $_ } + grep { $_->port } + $device->device_ips()->all; + foreach my $map (@{ setting('hide_deviceports')}) { next unless ref {} eq ref $map; foreach my $key (sort keys %$map) { # lhs matches device, rhs matches port - next unless check_acl_no($device, $key); + next unless $key and $map->{$key}; + next unless acl_matches($device, $key); - PORT: foreach my $port (sort keys %$device_ips) { - foreach my $thing (@{ $device_ips->{$port} }) { - next unless check_acl_no($thing, $map->{$key}); - - @results = grep { $_->port ne $port } @results; - next PORT; - } + foreach my $port (sort keys %$port_map) { + next unless acl_matches($port_map->{$port}, $map->{$key}); + ++$to_hide{$port}; } } } + + @results = grep { ! exists $to_hide{$_->port} } @results; } # sort ports diff --git a/lib/App/Netdisco/Worker/Plugin.pm b/lib/App/Netdisco/Worker/Plugin.pm index b263618f..64bab424 100644 --- a/lib/App/Netdisco/Worker/Plugin.pm +++ b/lib/App/Netdisco/Worker/Plugin.pm @@ -3,7 +3,7 @@ package App::Netdisco::Worker::Plugin; use Dancer ':syntax'; use Dancer::Plugin; -use App::Netdisco::Util::Permission qw/check_acl_no check_acl_only/; +use App::Netdisco::Util::Permission qw/acl_matches acl_matches_only/; use aliased 'App::Netdisco::Worker::Status'; use Scope::Guard 'guard'; use Storable 'dclone'; @@ -73,8 +73,8 @@ register 'register_worker' => sub { my $only = (exists $workerconf->{only} ? $workerconf->{only} : undef); return $job->add_status( Status->info('skip: acls restricted') ) - if ($no and check_acl_no($job->device, $no)) - or ($only and not check_acl_only($job->device, $only)); + if ($no and acl_matches($job->device, $no)) + or ($only and not acl_matches_only($job->device, $only)); # reduce device_auth by driver and action filters foreach my $stanza (@userconf) { diff --git a/lib/App/Netdisco/Worker/Plugin/Arpnip/Hooks.pm b/lib/App/Netdisco/Worker/Plugin/Arpnip/Hooks.pm index d50822a1..67ae6c3c 100644 --- a/lib/App/Netdisco/Worker/Plugin/Arpnip/Hooks.pm +++ b/lib/App/Netdisco/Worker/Plugin/Arpnip/Hooks.pm @@ -5,7 +5,7 @@ use App::Netdisco::Worker::Plugin; use aliased 'App::Netdisco::Worker::Status'; use App::Netdisco::Util::Worker; -use App::Netdisco::Util::Permission qw/check_acl_no check_acl_only/; +use App::Netdisco::Util::Permission qw/acl_matches acl_matches_only/; register_worker({ phase => 'late' }, sub { my ($job, $workerconf) = @_; @@ -21,8 +21,8 @@ register_worker({ phase => 'late' }, sub { my $no = ($conf->{'filter'}->{'no'} || []); my $only = ($conf->{'filter'}->{'only'} || []); - next if check_acl_no( $job->device, $no ); - next unless check_acl_only( $job->device, $only); + next if acl_matches( $job->device, $no ); + next unless acl_matches_only( $job->device, $only); if ($conf->{'event'} eq 'arpnip') { $count += queue_hook('arpnip', $conf); diff --git a/lib/App/Netdisco/Worker/Plugin/Arpnip/Subnets.pm b/lib/App/Netdisco/Worker/Plugin/Arpnip/Subnets.pm index d3073e28..e574c14f 100644 --- a/lib/App/Netdisco/Worker/Plugin/Arpnip/Subnets.pm +++ b/lib/App/Netdisco/Worker/Plugin/Arpnip/Subnets.pm @@ -5,7 +5,7 @@ use App::Netdisco::Worker::Plugin; use aliased 'App::Netdisco::Worker::Status'; use App::Netdisco::Transport::SNMP (); -use App::Netdisco::Util::Permission 'check_acl_no'; +use App::Netdisco::Util::Permission 'acl_matches'; use Dancer::Plugin::DBIC 'schema'; use NetAddr::IP::Lite ':lower'; use Time::HiRes 'gettimeofday'; @@ -42,7 +42,7 @@ sub gather_subnets { my $addr = $ip->addr; next if $addr eq '0.0.0.0'; - next if check_acl_no($ip, 'group:__LOOPBACK_ADDRESSES__'); + next if acl_matches($ip, 'group:__LOOPBACK_ADDRESSES__'); next if setting('ignore_private_nets') and $ip->is_rfc1918; my $netmask = $ip_netmask->{$addr} || $ip->bits(); diff --git a/lib/App/Netdisco/Worker/Plugin/Delete/Hooks.pm b/lib/App/Netdisco/Worker/Plugin/Delete/Hooks.pm index c8571cb5..05c4969c 100644 --- a/lib/App/Netdisco/Worker/Plugin/Delete/Hooks.pm +++ b/lib/App/Netdisco/Worker/Plugin/Delete/Hooks.pm @@ -5,7 +5,7 @@ use App::Netdisco::Worker::Plugin; use aliased 'App::Netdisco::Worker::Status'; use App::Netdisco::Util::Worker; -use App::Netdisco::Util::Permission qw/check_acl_no check_acl_only/; +use App::Netdisco::Util::Permission qw/acl_matches acl_matches_only/; register_worker({ phase => 'late' }, sub { my ($job, $workerconf) = @_; @@ -21,8 +21,8 @@ register_worker({ phase => 'late' }, sub { my $no = ($conf->{'filter'}->{'no'} || []); my $only = ($conf->{'filter'}->{'only'} || []); - next if check_acl_no( $job->device, $no ); - next unless check_acl_only( $job->device, $only); + next if acl_matches( $job->device, $no ); + next unless acl_matches_only( $job->device, $only); if ($conf->{'event'} eq 'delete') { $count += queue_hook('delete', $conf); diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/CanonicalIP.pm b/lib/App/Netdisco/Worker/Plugin/Discover/CanonicalIP.pm index a12f274f..3fed5c0e 100644 --- a/lib/App/Netdisco/Worker/Plugin/Discover/CanonicalIP.pm +++ b/lib/App/Netdisco/Worker/Plugin/Discover/CanonicalIP.pm @@ -5,7 +5,7 @@ use App::Netdisco::Worker::Plugin; use aliased 'App::Netdisco::Worker::Status'; use App::Netdisco::Transport::SNMP (); -use App::Netdisco::Util::Permission 'check_acl_no'; +use App::Netdisco::Util::Permission 'acl_matches'; use App::Netdisco::Util::DNS 'ipv4_from_hostname'; use App::Netdisco::Util::Device 'is_discoverable'; use Dancer::Plugin::DBIC 'schema'; @@ -34,37 +34,39 @@ register_worker({ phase => 'main', driver => 'snmp' }, sub { if (scalar @{ setting('device_identity') }) { my @idmaps = @{ setting('device_identity') }; - my $devips = $device->device_ips->order_by('alias'); + my @devips = $device->device_ips->order_by('alias')->all; - ALIAS: while (my $alias = $devips->next) { - next if $alias->alias eq $old_ip; + # using ALIASMAP break so that we stop after first successful renumber - foreach my $map (@idmaps) { - next unless ref {} eq ref $map; + ALIASMAP: foreach my $map (@idmaps) { + next unless ref {} eq ref $map; - foreach my $key (sort keys %$map) { - # lhs matches device, rhs matches device_ip - if (check_acl_no($device, $key) - and check_acl_no($alias, $map->{$key})) { + foreach my $key (sort keys %$map) { + # lhs matches device, rhs matches device_ip + next unless $key and $map->{$key}; + next unless acl_matches($device, $key); - if (not is_discoverable( $alias->alias )) { - debug sprintf ' [%s] device - cannot renumber to %s - not discoverable', - $old_ip, $alias->alias; - next; - } + foreach my $alias (@devips) { + next if $alias->alias eq $old_ip; + next unless acl_matches($alias, $map->{$key}); - if (App::Netdisco::Transport::SNMP->test_connection( $alias->alias )) { - $new_ip = $alias->alias; - last ALIAS; - } - else { - debug sprintf ' [%s] device - cannot renumber to %s - SNMP connect failed', - $old_ip, $alias->alias; - } + if (not is_discoverable( $alias->alias )) { + debug sprintf ' [%s] device - cannot renumber to %s - not discoverable', + $old_ip, $alias->alias; + next; + } + + if (App::Netdisco::Transport::SNMP->test_connection( $alias->alias )) { + $new_ip = $alias->alias; + last ALIASMAP; + } + else { + debug sprintf ' [%s] device - cannot renumber to %s - SNMP connect failed', + $old_ip, $alias->alias; } } } - } # ALIAS + } } return if $new_ip eq $old_ip; diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/Hooks.pm b/lib/App/Netdisco/Worker/Plugin/Discover/Hooks.pm index c125f034..02fb026d 100644 --- a/lib/App/Netdisco/Worker/Plugin/Discover/Hooks.pm +++ b/lib/App/Netdisco/Worker/Plugin/Discover/Hooks.pm @@ -5,7 +5,7 @@ use App::Netdisco::Worker::Plugin; use aliased 'App::Netdisco::Worker::Status'; use App::Netdisco::Util::Worker; -use App::Netdisco::Util::Permission qw/check_acl_no check_acl_only/; +use App::Netdisco::Util::Permission qw/acl_matches acl_matches_only/; register_worker({ phase => 'late' }, sub { my ($job, $workerconf) = @_; @@ -21,8 +21,8 @@ register_worker({ phase => 'late' }, sub { my $no = ($conf->{'filter'}->{'no'} || []); my $only = ($conf->{'filter'}->{'only'} || []); - next if check_acl_no( $job->device, $no ); - next unless check_acl_only( $job->device, $only); + next if acl_matches( $job->device, $no ); + next unless acl_matches_only( $job->device, $only); if (vars->{'new_device'} and $conf->{'event'} eq 'new_device') { $count += queue_hook('new_device', $conf); diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/Neighbors.pm b/lib/App/Netdisco/Worker/Plugin/Discover/Neighbors.pm index 4447d8d3..bea8a3b6 100644 --- a/lib/App/Netdisco/Worker/Plugin/Discover/Neighbors.pm +++ b/lib/App/Netdisco/Worker/Plugin/Discover/Neighbors.pm @@ -6,7 +6,7 @@ use aliased 'App::Netdisco::Worker::Status'; use App::Netdisco::Transport::SNMP (); use App::Netdisco::Util::Device qw/get_device is_discoverable/; -use App::Netdisco::Util::Permission 'check_acl_no'; +use App::Netdisco::Util::Permission 'acl_matches'; use App::Netdisco::JobQueue 'jq_insert'; use Dancer::Plugin::DBIC 'schema'; use List::MoreUtils (); @@ -185,7 +185,7 @@ sub store_neighbors { # useable remote IP... if ((! $r_netaddr) or ($remote_ip eq '0.0.0.0') or - check_acl_no($remote_ip, 'group:__LOOPBACK_ADDRESSES__')) { + acl_matches($remote_ip, 'group:__LOOPBACK_ADDRESSES__')) { if ($remote_id) { my $devices = schema('netdisco')->resultset('Device'); diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/Neighbors/Routed.pm b/lib/App/Netdisco/Worker/Plugin/Discover/Neighbors/Routed.pm index 46f85d70..00889d8f 100644 --- a/lib/App/Netdisco/Worker/Plugin/Discover/Neighbors/Routed.pm +++ b/lib/App/Netdisco/Worker/Plugin/Discover/Neighbors/Routed.pm @@ -6,7 +6,7 @@ use App::Netdisco::Transport::SNMP; use aliased 'App::Netdisco::Worker::Status'; use App::Netdisco::Util::Device qw/get_device is_discoverable/; -use App::Netdisco::Util::Permission 'check_acl_no'; +use App::Netdisco::Util::Permission 'acl_matches'; use App::Netdisco::JobQueue 'jq_insert'; register_worker({ phase => 'main', driver => 'snmp' }, sub { @@ -15,8 +15,8 @@ register_worker({ phase => 'main', driver => 'snmp' }, sub { my $device = $job->device; return unless $device->in_storage and ($device->has_layer(3) - or check_acl_no($device, 'force_macsuck') - or check_acl_no($device, 'ignore_layers')); + or acl_matches($device, 'force_arpnip') + or acl_matches($device, 'ignore_layers')); my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) or return Status->defer("discover failed: could not SNMP connect to $device"); diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/PortProperties.pm b/lib/App/Netdisco/Worker/Plugin/Discover/PortProperties.pm index 20baaee1..600d2a41 100644 --- a/lib/App/Netdisco/Worker/Plugin/Discover/PortProperties.pm +++ b/lib/App/Netdisco/Worker/Plugin/Discover/PortProperties.pm @@ -8,6 +8,8 @@ use App::Netdisco::Transport::SNMP (); use Dancer::Plugin::DBIC 'schema'; use Encode; +use App::Netdisco::Util::Web 'sort_port'; +use App::Netdisco::Util::Permission 'acl_matches'; use App::Netdisco::Util::FastResolver 'hostnames_resolve_async'; use App::Netdisco::Util::Device qw/is_discoverable match_to_setting/; @@ -131,6 +133,28 @@ register_worker({ phase => 'main', driver => 'snmp' }, sub { $properties{ $port }->{remote_serial} = $rem_serial->{ $idx }; } + if (scalar @{ setting('ignore_deviceports') }) { + foreach my $map (@{ setting('ignore_deviceports')}) { + next unless ref {} eq ref $map; + + foreach my $key (sort keys %$map) { + # lhs matches device, rhs matches port + next unless $key and $map->{$key}; + next unless acl_matches($device, $key); + + foreach my $port (sort { sort_port($a, $b) } keys %properties) { + next unless acl_matches([$properties{$port}, $device_ports->{$port}], + $map->{$key}); + + debug sprintf ' [%s] properties - removing %s (config:ignore_deviceports)', + $device->ip, $port; + $device_ports->{$port}->delete; # like, for real, in the DB + delete $properties{$port}; + } + } + } + } + foreach my $idx (keys %$interfaces) { next unless defined $idx; my $port = $interfaces->{$idx} or next; @@ -155,7 +179,7 @@ register_worker({ phase => 'main', driver => 'snmp' }, sub { schema('netdisco')->txn_do(sub { my $gone = $device->properties_ports->delete; - debug sprintf ' [%s] properties - removed %d ports with properties', + debug sprintf ' [%s] properties - removed %d port properties', $device->ip, $gone; $device->properties_ports->populate( [map {{ port => $_, %{ $properties{$_} } }} keys %properties] ); diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/Properties.pm b/lib/App/Netdisco/Worker/Plugin/Discover/Properties.pm index 7deaffde..751629c0 100644 --- a/lib/App/Netdisco/Worker/Plugin/Discover/Properties.pm +++ b/lib/App/Netdisco/Worker/Plugin/Discover/Properties.pm @@ -5,14 +5,16 @@ use App::Netdisco::Worker::Plugin; use aliased 'App::Netdisco::Worker::Status'; use App::Netdisco::Transport::SNMP (); -use App::Netdisco::Util::Permission qw/check_acl_no check_acl_only/; +use App::Netdisco::Util::Permission qw/acl_matches acl_matches_only/; use App::Netdisco::Util::FastResolver 'hostnames_resolve_async'; use App::Netdisco::Util::Device 'get_device'; use App::Netdisco::Util::DNS 'hostname_from_ip'; use App::Netdisco::Util::SNMP 'snmp_comm_reindex'; +use App::Netdisco::Util::Web 'sort_port'; use Dancer::Plugin::DBIC 'schema'; use Scope::Guard 'guard'; use NetAddr::IP::Lite ':lower'; +use Storable 'dclone'; use Encode; register_worker({ phase => 'early', driver => 'snmp' }, sub { @@ -66,7 +68,7 @@ register_worker({ phase => 'early', driver => 'snmp' }, sub { my $protect = setting('snmp_field_protection')->{'device'} || {}; my %dirty = $device->get_dirty_columns; foreach my $field (keys %dirty) { - next unless check_acl_only($ip, $protect->{$field}); + next unless acl_matches_only($ip, $protect->{$field}); if (!defined $dirty{$field} or $dirty{$field} eq '') { return $job->cancel("discover cancelled: $ip failed to return valid $field"); } @@ -182,13 +184,6 @@ register_worker({ phase => 'early', driver => 'snmp' }, sub { my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) or return Status->defer("discover failed: could not SNMP connect to $device"); - # gather device_ips for use in ACLs later - my $device_ips = {}; - foreach my $dip ($device->device_ips()->all) { - next unless defined $dip->port and $dip->port; - push @{ $device_ips->{ $dip->port } }, $dip; - } - my $interfaces = $snmp->interfaces; my $i_type = $snmp->i_type; my $i_ignore = $snmp->i_ignore; @@ -272,38 +267,29 @@ register_worker({ phase => 'early', driver => 'snmp' }, sub { } if (scalar @{ setting('ignore_deviceports') }) { - foreach my $port (keys %$device_ips) { - if (!exists $deviceports{$port}) { - delete $device_ips->{$port}; - next; - } - foreach my $dip (@{ $device_ips->{$port} }) { - $dip->set_inflated_columns({ device_port => $deviceports{$port} }); - } - } - foreach my $port (keys %deviceports) { - next if exists $device_ips->{$port}; - push @{ $device_ips->{$port} }, - schema('netdisco')->resultset('DevicePort') - ->new_result( $deviceports{$port} ); - } + my $port_map = {}; + + map { push @{ $port_map->{ $_->port } }, $_ } + grep { $_->port } + $device->device_ips()->all; + + map { push @{ $port_map->{ $_->{port} } }, $_ } + values %{ dclone (\%deviceports || {}) }; foreach my $map (@{ setting('ignore_deviceports')}) { next unless ref {} eq ref $map; foreach my $key (sort keys %$map) { # lhs matches device, rhs matches port - next unless check_acl_no($device, $key); + next unless $key and $map->{$key}; + next unless acl_matches($device, $key); - PORT: foreach my $port (sort keys %$device_ips) { - foreach my $thing (@{ $device_ips->{$port} }) { - next unless check_acl_no($thing, $map->{$key}); + foreach my $port (sort { sort_port( $a, $b) } keys %$port_map) { + next unless acl_matches($port_map->{$port}, $map->{$key}); - debug sprintf ' [%s] interfaces - ignoring %s (config:ignore_deviceports)', - $device->ip, $port; - delete $deviceports{$port}; - next PORT; - } + debug sprintf ' [%s] interfaces - ignoring %s (config:ignore_deviceports)', + $device->ip, $port; + delete $deviceports{$port}; } } } @@ -446,7 +432,7 @@ sub _get_ipv4_aliases { my $addr = $ip->addr; next if $addr eq '0.0.0.0'; - next if check_acl_no($ip, 'group:__LOOPBACK_ADDRESSES__'); + next if acl_matches($ip, 'group:__LOOPBACK_ADDRESSES__'); next if setting('ignore_private_nets') and $ip->is_rfc1918; my $iid = $ip_index->{$addr}; @@ -499,7 +485,7 @@ sub _get_ipv6_aliases { my $addr = $ip->addr; next if $addr eq '::0'; - next if check_acl_no($ip, 'group:__LOOPBACK_ADDRESSES__'); + next if acl_matches($ip, 'group:__LOOPBACK_ADDRESSES__'); my $port = $interfaces->{ $ipv6_index->{$iid} }; my $subnet = $ipv6_pfxlen->{$iid} diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/WithNodes.pm b/lib/App/Netdisco/Worker/Plugin/Discover/WithNodes.pm index b41b471a..28f7fba2 100644 --- a/lib/App/Netdisco/Worker/Plugin/Discover/WithNodes.pm +++ b/lib/App/Netdisco/Worker/Plugin/Discover/WithNodes.pm @@ -5,7 +5,7 @@ use App::Netdisco::Worker::Plugin; use aliased 'App::Netdisco::Worker::Status'; use App::Netdisco::JobQueue 'jq_insert'; -use App::Netdisco::Util::Permission 'check_acl_no'; +use App::Netdisco::Util::Permission 'acl_matches'; register_worker({ phase => 'main' }, sub { my ($job, $workerconf) = @_; @@ -16,8 +16,8 @@ register_worker({ phase => 'main' }, sub { return unless $device->in_storage and $job->subaction eq 'with-nodes'; if (!defined $device->last_macsuck and ($device->has_layer(2) - or check_acl_no($device, 'force_macsuck') - or check_acl_no($device, 'ignore_layers'))) { + or acl_matches($device, 'force_macsuck') + or acl_matches($device, 'ignore_layers'))) { jq_insert({ device => $device->ip, action => 'macsuck', @@ -28,8 +28,8 @@ register_worker({ phase => 'main' }, sub { } if (!defined $device->last_arpnip and ($device->has_layer(3) - or check_acl_no($device, 'force_arpnip') - or check_acl_no($device, 'ignore_layers'))) { + or acl_matches($device, 'force_arpnip') + or acl_matches($device, 'ignore_layers'))) { jq_insert({ device => $device->ip, action => 'arpnip', diff --git a/lib/App/Netdisco/Worker/Plugin/Macsuck/Hooks.pm b/lib/App/Netdisco/Worker/Plugin/Macsuck/Hooks.pm index 1cf5b52e..d40a47c1 100644 --- a/lib/App/Netdisco/Worker/Plugin/Macsuck/Hooks.pm +++ b/lib/App/Netdisco/Worker/Plugin/Macsuck/Hooks.pm @@ -5,7 +5,7 @@ use App::Netdisco::Worker::Plugin; use aliased 'App::Netdisco::Worker::Status'; use App::Netdisco::Util::Worker; -use App::Netdisco::Util::Permission qw/check_acl_no check_acl_only/; +use App::Netdisco::Util::Permission qw/acl_matches acl_matches_only/; register_worker({ phase => 'late' }, sub { my ($job, $workerconf) = @_; @@ -21,8 +21,8 @@ register_worker({ phase => 'late' }, sub { my $no = ($conf->{'filter'}->{'no'} || []); my $only = ($conf->{'filter'}->{'only'} || []); - next if check_acl_no( $job->device, $no ); - next unless check_acl_only( $job->device, $only); + next if acl_matches( $job->device, $no ); + next unless acl_matches_only( $job->device, $only); if ($conf->{'event'} eq 'macsuck') { $count += queue_hook('macsuck', $conf); diff --git a/lib/App/Netdisco/Worker/Plugin/Macsuck/Nodes.pm b/lib/App/Netdisco/Worker/Plugin/Macsuck/Nodes.pm index 2b4300fc..2519d468 100644 --- a/lib/App/Netdisco/Worker/Plugin/Macsuck/Nodes.pm +++ b/lib/App/Netdisco/Worker/Plugin/Macsuck/Nodes.pm @@ -5,11 +5,12 @@ use App::Netdisco::Worker::Plugin; use aliased 'App::Netdisco::Worker::Status'; use App::Netdisco::Transport::SNMP (); -use App::Netdisco::Util::Permission qw/check_acl_no check_acl_only/; +use App::Netdisco::Util::Permission 'acl_matches'; use App::Netdisco::Util::PortMAC 'get_port_macs'; use App::Netdisco::Util::Device 'match_to_setting'; use App::Netdisco::Util::Node 'check_mac'; use App::Netdisco::Util::SNMP 'snmp_comm_reindex'; +use App::Netdisco::Util::Web 'sort_port'; use Dancer::Plugin::DBIC 'schema'; use Time::HiRes 'gettimeofday'; @@ -37,7 +38,8 @@ register_worker({ phase => 'early', # cache the device ports to save hitting the database for many single rows vars->{'device_ports'} = {map {($_->port => $_)} - $device->ports(undef, {prefetch => {neighbor_alias => 'device'}})->all}; + $device->ports(undef, {prefetch => ['properties', + {neighbor_alias => 'device'}]})->all}; }); register_worker({ phase => 'main', driver => 'direct', @@ -488,14 +490,15 @@ sub sanity_macs { foreach my $key (sort keys %$map) { # lhs matches device, rhs matches port - next unless check_acl_only($device, $key); + next unless $key and $map->{$key}; + next unless acl_matches($device, $key); - foreach my $port (keys %{ $device_ports }) { - next unless check_acl_only($device_ports->{$port}, $map->{$key}); + foreach my $port (sort { sort_port($a, $b) } keys %{ $device_ports }) { + next unless acl_matches($device_ports->{$port}, $map->{$key}); - ++$ignoreport->{$port}; debug sprintf ' [%s] macsuck %s - port suppressed by macsuck_no_deviceports', $device->ip, $port; + ++$ignoreport->{$port}; } } } @@ -559,7 +562,7 @@ sub sanity_macs { # with device lookups. my $neigh_cannot_macsuck = eval { # can fail - check_acl_no(($device_port->neighbor || "0 but true"), 'macsuck_unsupported') || + acl_matches(($device_port->neighbor || "0 but true"), 'macsuck_unsupported') || match_to_setting($device_port->remote_type, 'macsuck_unsupported_type') }; # here, is_uplink comes from Discover::Neighbors finding LLDP remnants diff --git a/lib/App/Netdisco/Worker/Plugin/MakeRancidConf.pm b/lib/App/Netdisco/Worker/Plugin/MakeRancidConf.pm index de9ea171..810e842f 100644 --- a/lib/App/Netdisco/Worker/Plugin/MakeRancidConf.pm +++ b/lib/App/Netdisco/Worker/Plugin/MakeRancidConf.pm @@ -12,7 +12,7 @@ use aliased 'App::Netdisco::Worker::Status'; use Path::Class; use List::Util qw/pairkeys pairfirst/; use File::Slurper qw/read_lines write_text/; -use App::Netdisco::Util::Permission 'check_acl_no'; +use App::Netdisco::Util::Permission 'acl_matches'; register_worker({ phase => 'main' }, sub { my ($job, $workerconf) = @_; @@ -65,16 +65,16 @@ register_worker({ phase => 'main' }, sub { my $routerunicity = {}; while (my $d = $devices->next) { - if (check_acl_no($d, $config->{excluded})) { + if (acl_matches($d, $config->{excluded})) { debug " skipping $d: device excluded of export"; next } - my $name = check_acl_no($d, $config->{by_ip}) ? $d->ip : ($d->dns || $d->name); - $name =~ s/$domain_suffix$// if check_acl_no($d, $config->{by_hostname}); + my $name = acl_matches($d, $config->{by_ip}) ? $d->ip : ($d->dns || $d->name); + $name =~ s/$domain_suffix$// if acl_matches($d, $config->{by_hostname}); my ($group) = - (pairkeys pairfirst { check_acl_no($d, $b) } %{ $config->{groups} }) || $default_group; + (pairkeys pairfirst { acl_matches($d, $b) } %{ $config->{groups} }) || $default_group; if (exists($routerunicity->{$group}->{$name})) { debug " skipping $d: device excluded because already present in export list"; @@ -82,7 +82,7 @@ register_worker({ phase => 'main' }, sub { } my ($vendor) = - (pairkeys pairfirst { check_acl_no($d, $b) } %{ $config->{vendormap} }) + (pairkeys pairfirst { acl_matches($d, $b) } %{ $config->{vendormap} }) || $d->vendor; if (not ($name and $vendor)) { diff --git a/lib/App/Netdisco/Worker/Runner.pm b/lib/App/Netdisco/Worker/Runner.pm index 3ced68da..e4edf6ce 100644 --- a/lib/App/Netdisco/Worker/Runner.pm +++ b/lib/App/Netdisco/Worker/Runner.pm @@ -3,7 +3,7 @@ package App::Netdisco::Worker::Runner; use Dancer qw/:moose :syntax/; use Dancer::Plugin::DBIC 'schema'; use App::Netdisco::Util::Device 'get_device'; -use App::Netdisco::Util::Permission qw/check_acl_no check_acl_only/; +use App::Netdisco::Util::Permission qw/acl_matches acl_matches_only/; use aliased 'App::Netdisco::Worker::Status'; use Try::Tiny; @@ -43,8 +43,8 @@ sub run { my $no = (exists $stanza->{no} ? $stanza->{no} : undef); my $only = (exists $stanza->{only} ? $stanza->{only} : undef); - next if $no and check_acl_no($job->device, $no); - next if $only and not check_acl_only($job->device, $only); + next if $no and acl_matches($job->device, $no); + next if $only and not acl_matches_only($job->device, $only); push @newuserconf, dclone $stanza; } diff --git a/xt/20-checkacl.t b/xt/20-checkacl.t index 86f8268b..226b5014 100644 --- a/xt/20-checkacl.t +++ b/xt/20-checkacl.t @@ -8,8 +8,8 @@ use Test::More 1.302083; use Test::File::ShareDir::Dist { 'App-Netdisco' => 'share/' }; BEGIN { - use_ok( 'App::Netdisco::Configuration', 'check_acl' ); - use_ok( 'App::Netdisco::Util::Permission', 'check_acl' ); + use_ok( 'App::Netdisco::Configuration', 'acl_matches' ); + use_ok( 'App::Netdisco::Util::Permission', 'acl_matches' ); } use Dancer qw/:script !pass/; @@ -37,79 +37,79 @@ my @conf = ( ); # name, ipv4, ipv6, v4 prefix, v6 prefix -ok(check_acl('localhost',[$conf[0]]), 'same name'); -ok(check_acl('127.0.0.1',[$conf[2]]), 'same ipv4'); -ok(check_acl('::1',[$conf[4]]), 'same ipv6'); -ok(check_acl('127.0.0.0/29',[$conf[6]]), 'same v4 prefix'); -ok(check_acl('::1/128',[$conf[8]]), 'same v6 prefix'); +ok(acl_matches('localhost',[$conf[0]]), 'same name'); +ok(acl_matches('127.0.0.1',[$conf[2]]), 'same ipv4'); +ok(acl_matches('::1',[$conf[4]]), 'same ipv6'); +ok(acl_matches('127.0.0.0/29',[$conf[6]]), 'same v4 prefix'); +ok(acl_matches('::1/128',[$conf[8]]), 'same v6 prefix'); # failed name, ipv4, ipv6, v4 prefix, v6 prefix -is(check_acl('www.microsoft.com',[$conf[0]]), 0, 'failed name'); -is(check_acl('172.20.0.1',[$conf[2]]), 0, 'failed ipv4'); -is(check_acl('2001:db8::5',[$conf[4]]), 0, 'failed ipv6'); -is(check_acl('172.16.1.3/29',[$conf[6]]), 0, 'failed v4 prefix'); -is(check_acl('2001:db8:f00d::/64',[$conf[8]]), 0, 'failed v6 prefix'); +is(acl_matches('www.microsoft.com',[$conf[0]]), 0, 'failed name'); +is(acl_matches('172.20.0.1',[$conf[2]]), 0, 'failed ipv4'); +is(acl_matches('2001:db8::5',[$conf[4]]), 0, 'failed ipv6'); +is(acl_matches('172.16.1.3/29',[$conf[6]]), 0, 'failed v4 prefix'); +is(acl_matches('2001:db8:f00d::/64',[$conf[8]]), 0, 'failed v6 prefix'); # negated name, ipv4, ipv6, v4 prefix, v6 prefix -ok(check_acl('localhost',[$conf[1]]), 'not same name'); -ok(check_acl('127.0.0.1',[$conf[3]]), 'not same ipv4'); -ok(check_acl('::1',[$conf[5]]), 'not same ipv6'); -ok(check_acl('127.0.0.0/29',[$conf[7]]), 'not same v4 prefix'); -ok(check_acl('::1/128',[$conf[9]]), 'not same v6 prefix'); +ok(acl_matches('localhost',[$conf[1]]), 'not same name'); +ok(acl_matches('127.0.0.1',[$conf[3]]), 'not same ipv4'); +ok(acl_matches('::1',[$conf[5]]), 'not same ipv6'); +ok(acl_matches('127.0.0.0/29',[$conf[7]]), 'not same v4 prefix'); +ok(acl_matches('::1/128',[$conf[9]]), 'not same v6 prefix'); # v4 range, v6 range -ok(check_acl('127.0.0.1',[$conf[10]]), 'in v4 range'); -ok(check_acl('::1',[$conf[12]]), 'in v6 range'); +ok(acl_matches('127.0.0.1',[$conf[10]]), 'in v4 range'); +ok(acl_matches('::1',[$conf[12]]), 'in v6 range'); # failed v4 range, v6 range -is(check_acl('172.20.0.1',[$conf[10]]), 0, 'failed v4 range'); -is(check_acl('2001:db8::5',[$conf[12]]), 0, 'failed v6 range'); +is(acl_matches('172.20.0.1',[$conf[10]]), 0, 'failed v4 range'); +is(acl_matches('2001:db8::5',[$conf[12]]), 0, 'failed v6 range'); # negated v4 range, v6 range -ok(check_acl('127.0.0.1',[$conf[11]]), 'not in v4 range'); -ok(check_acl('::1',[$conf[13]]), 'not in v6 range'); +ok(acl_matches('127.0.0.1',[$conf[11]]), 'not in v4 range'); +ok(acl_matches('::1',[$conf[13]]), 'not in v6 range'); # hostname regexp -# FIXME ok(check_acl('localhost',[$conf[14]]), 'name regexp'); -# FIXME ok(check_acl('127.0.0.1',[$conf[14]]), 'IP regexp'); -is(check_acl('www.google.com',[$conf[14]]), 0, 'failed regexp'); +# FIXME ok(acl_matches('localhost',[$conf[14]]), 'name regexp'); +# FIXME ok(acl_matches('127.0.0.1',[$conf[14]]), 'IP regexp'); +is(acl_matches('www.google.com',[$conf[14]]), 0, 'failed regexp'); # OR of prefix, range, regexp, property (2 of, 3 of, 4 of) -ok(check_acl('127.0.0.1',[@conf[8,0]]), 'OR: prefix, name'); -ok(check_acl('127.0.0.1',[@conf[8,12,0]]), 'OR: prefix, range, name'); -ok(check_acl('127.0.0.1',[@conf[8,12,15,0]]), 'OR: prefix, range, regexp, name'); +ok(acl_matches('127.0.0.1',[@conf[8,0]]), 'OR: prefix, name'); +ok(acl_matches('127.0.0.1',[@conf[8,12,0]]), 'OR: prefix, range, name'); +ok(acl_matches('127.0.0.1',[@conf[8,12,15,0]]), 'OR: prefix, range, regexp, name'); # OR of negated prefix, range, regexp, property (2 of, 3 of, 4 of) -ok(check_acl('127.0.0.1',[@conf[17,0]]), 'OR: !prefix, name'); -ok(check_acl('127.0.0.1',[@conf[17,18,0]]), 'OR: !prefix, !range, name'); -ok(check_acl('127.0.0.1',[@conf[17,18,19,0]]), 'OR: !prefix, !range, !regexp, name'); +ok(acl_matches('127.0.0.1',[@conf[17,0]]), 'OR: !prefix, name'); +ok(acl_matches('127.0.0.1',[@conf[17,18,0]]), 'OR: !prefix, !range, name'); +ok(acl_matches('127.0.0.1',[@conf[17,18,19,0]]), 'OR: !prefix, !range, !regexp, name'); # AND of prefix, range, regexp, property (2 of, 3 of, 4 of) -ok(check_acl('127.0.0.1',[@conf[6,0,20]]), 'AND: prefix, name'); -ok(check_acl('127.0.0.1',[@conf[6,10,0,20]]), 'AND: prefix, range, name'); -# FIXME ok(check_acl('127.0.0.1',[@conf[6,10,14,0,20]]), 'AND: prefix, range, regexp, name'); +ok(acl_matches('127.0.0.1',[@conf[6,0,20]]), 'AND: prefix, name'); +ok(acl_matches('127.0.0.1',[@conf[6,10,0,20]]), 'AND: prefix, range, name'); +# FIXME ok(acl_matches('127.0.0.1',[@conf[6,10,14,0,20]]), 'AND: prefix, range, regexp, name'); # failed AND on prefix, range, regexp -is(check_acl('127.0.0.1',[@conf[8,10,14,0,20]]), 0, 'failed AND: prefix!, range, regexp, name'); -is(check_acl('127.0.0.1',[@conf[6,12,14,0,20]]), 0, 'failed AND: prefix, range!, regexp, name'); -is(check_acl('127.0.0.1',[@conf[6,10,15,0,20]]), 0, 'failed AND: prefix, range, regexp!, name'); +is(acl_matches('127.0.0.1',[@conf[8,10,14,0,20]]), 0, 'failed AND: prefix!, range, regexp, name'); +is(acl_matches('127.0.0.1',[@conf[6,12,14,0,20]]), 0, 'failed AND: prefix, range!, regexp, name'); +is(acl_matches('127.0.0.1',[@conf[6,10,15,0,20]]), 0, 'failed AND: prefix, range, regexp!, name'); # AND of negated prefix, range, regexp, property (2 of, 3 of, 4 of) -ok(check_acl('127.0.0.1',[@conf[9,0,20]]), 'AND: !prefix, name'); -ok(check_acl('127.0.0.1',[@conf[7,11,0,20]]), 'AND: !prefix, !range, name'); -ok(check_acl('127.0.0.1',[@conf[9,13,16,0,20]]), 'AND: !prefix, !range, !regexp, name'); +ok(acl_matches('127.0.0.1',[@conf[9,0,20]]), 'AND: !prefix, name'); +ok(acl_matches('127.0.0.1',[@conf[7,11,0,20]]), 'AND: !prefix, !range, name'); +ok(acl_matches('127.0.0.1',[@conf[9,13,16,0,20]]), 'AND: !prefix, !range, !regexp, name'); # group ref -is(check_acl('192.0.2.1',[$conf[22]]), 1, '!missing group ref'); -is(check_acl('192.0.2.1',[$conf[21]]), 0, 'failed missing group ref'); +is(acl_matches('192.0.2.1',[$conf[22]]), 1, '!missing group ref'); +is(acl_matches('192.0.2.1',[$conf[21]]), 0, 'failed missing group ref'); setting('host_groups')->{'groupreftest'} = ['192.0.2.1']; -is(check_acl('192.0.2.1',[$conf[21]]), 1, 'group ref'); -is(check_acl('192.0.2.1',[$conf[22]]), 0, 'failed !missing group ref'); +is(acl_matches('192.0.2.1',[$conf[21]]), 1, 'group ref'); +is(acl_matches('192.0.2.1',[$conf[22]]), 0, 'failed !missing group ref'); # scalar promoted to list -ok(check_acl('localhost',$conf[0]), 'scalar promoted'); -ok(check_acl('localhost',$conf[1]), 'not scalar promoted'); -is(check_acl('www.microsoft.com',$conf[0]), 0, 'failed scalar promoted'); +ok(acl_matches('localhost',$conf[0]), 'scalar promoted'); +ok(acl_matches('localhost',$conf[1]), 'not scalar promoted'); +is(acl_matches('www.microsoft.com',$conf[0]), 0, 'failed scalar promoted'); use App::Netdisco::DB; my $dip = App::Netdisco::DB->resultset('DeviceIp')->new_result({ @@ -125,15 +125,80 @@ my $dip = App::Netdisco::DB->resultset('DeviceIp')->new_result({ }); # device properties -ok(check_acl($dip, [$conf[23]]), 'instance anon property deviceport:alias'); -ok(check_acl($dip, ['ip:'.$conf[2]]), 'instance named property deviceport:ip'); -ok(check_acl($dip, ['!ip:'. $conf[23]]), 'negated instance named property deviceport:ip'); -is(check_acl($dip, ['port:'.$conf[2]]), 0, 'failed instance named property deviceport:ip'); -ok(check_acl($dip, ['port:.*GigabitEthernet.*']), 'instance named property regexp deviceport:port'); +ok(acl_matches($dip, [$conf[23]]), '1obj instance anon property deviceport:alias'); +ok(acl_matches($dip, ['ip:'.$conf[2]]), '1obj instance named property deviceport:ip'); +ok(acl_matches($dip, ['!ip:'. $conf[23]]), '1obj negated instance named property deviceport:ip'); +is(acl_matches($dip, ['port:'.$conf[2]]), 0, '1obj failed instance named property deviceport:ip'); +ok(acl_matches($dip, ['port:.*GigabitEthernet.*']), '1obj instance named property regexp deviceport:port'); -ok(check_acl($dip, ['type:l3ipvlan']), 'related item field match'); -ok(check_acl($dip, ['remote_ip:']), 'related item field empty'); -ok(check_acl($dip, ['!type:']), 'related item field not empty'); -is(check_acl($dip, ['foobar:xyz']), 0, 'unknown property'); +# DeviceIp no longer has DevicePort slot accessors +#ok(acl_matches($dip, ['type:l3ipvlan']), '1obj related item field match'); +#ok(acl_matches($dip, ['remote_ip:']), '1obj related item field empty'); +#ok(acl_matches($dip, ['!type:']), '1obj related item field not empty'); +#is(acl_matches($dip, ['foobar:xyz']), 0, '1obj unknown property'); + +my $dip2 = App::Netdisco::DB->resultset('DeviceIp')->new_result({ + ip => '127.0.0.1', + port => 'TenGigabitEthernet1/10', + alias => '192.0.2.1', +}); + +my $dp = App::Netdisco::DB->resultset('DevicePort')->new_result({ + ip => '127.0.0.1', + port => 'TenGigabitEthernet1/10', + type => 'l3ipvlan', +}); + +# device properties +ok(acl_matches([$dip2, $dp], [$conf[23]]), '2obj instance anon property deviceport:alias'); +ok(acl_matches([$dip2, $dp], ['ip:'.$conf[2]]), '2obj instance named property deviceport:ip'); +ok(acl_matches([undef, $dip2, $dp], ['ip:'.$conf[2]]), '2obj instance named property after undef'); +ok(acl_matches([$dip2, $dp], ['!ip:'. $conf[23]]), '2obj negated instance named property deviceport:ip'); +is(acl_matches([$dip2, $dp], ['port:'.$conf[2]]), 0, '2obj failed instance named property deviceport:ip'); +ok(acl_matches([$dip2, $dp], ['port:.*GigabitEthernet.*']), '2obj instance named property regexp deviceport:port'); + +ok(acl_matches([$dip2, $dp], ['type:l3ipvlan']), '2obj related item field match'); +ok(acl_matches([$dip2, $dp], ['remote_ip:']), '2obj related item field empty'); +ok(acl_matches([$dip2, $dp], ['!type:']), '2obj related item field not empty'); +is(acl_matches([$dip2, $dp], ['foobar:xyz']), 0, '2obj unknown property'); + +my $dip2c = { $dip2->get_inflated_columns }; +my $dpc = { $dp->get_inflated_columns }; + +# device properties +ok(acl_matches([$dip2c, $dpc], [$conf[23]]), 'hh instance anon property deviceport:alias'); +ok(acl_matches([$dip2c, $dpc], ['ip:'.$conf[2]]), 'hh instance named property deviceport:ip'); +ok(acl_matches([$dip2c, $dpc], ['!ip:'. $conf[23]]), 'hh negated instance named property deviceport:ip'); +is(acl_matches([$dip2c, $dpc], ['port:'.$conf[2]]), 0, 'hh failed instance named property deviceport:ip'); +ok(acl_matches([$dip2c, $dpc], ['port:.*GigabitEthernet.*']), 'hh instance named property regexp deviceport:port'); + +ok(acl_matches([$dip2c, $dpc], ['type:l3ipvlan']), 'hh related item field match'); +ok(acl_matches([$dip2c, $dpc], ['remote_ip:']), 'hh related item field empty'); +ok(acl_matches([$dip2c, $dpc], ['!type:']), 'hh related item field not empty'); +is(acl_matches([$dip2c, $dpc], ['foobar:xyz']), 0, 'hh unknown property'); + +# device properties +ok(acl_matches([$dip2, $dpc], [$conf[23]]), 'oh instance anon property deviceport:alias'); +ok(acl_matches([$dip2, $dpc], ['ip:'.$conf[2]]), 'oh instance named property deviceport:ip'); +ok(acl_matches([$dip2, $dpc], ['!ip:'. $conf[23]]), 'oh negated instance named property deviceport:ip'); +is(acl_matches([$dip2, $dpc], ['port:'.$conf[2]]), 0, 'oh failed instance named property deviceport:ip'); +ok(acl_matches([$dip2, $dpc], ['port:.*GigabitEthernet.*']), 'oh instance named property regexp deviceport:port'); + +ok(acl_matches([$dip2, $dpc], ['type:l3ipvlan']), 'oh related item field match'); +ok(acl_matches([$dip2, $dpc], ['remote_ip:']), 'oh related item field empty'); +ok(acl_matches([$dip2, $dpc], ['!type:']), 'oh related item field not empty'); +is(acl_matches([$dip2, $dpc], ['foobar:xyz']), 0, 'oh unknown property'); + +# device properties +ok(acl_matches([$dip2c, $dp], [$conf[23]]), 'ho instance anon property deviceport:alias'); +ok(acl_matches([$dip2c, $dp], ['ip:'.$conf[2]]), 'ho instance named property deviceport:ip'); +ok(acl_matches([$dip2c, $dp], ['!ip:'. $conf[23]]), 'ho negated instance named property deviceport:ip'); +is(acl_matches([$dip2c, $dp], ['port:'.$conf[2]]), 0, 'ho failed instance named property deviceport:ip'); +ok(acl_matches([$dip2c, $dp], ['port:.*GigabitEthernet.*']), 'ho instance named property regexp deviceport:port'); + +ok(acl_matches([$dip2c, $dp], ['type:l3ipvlan']), 'ho related item field match'); +ok(acl_matches([$dip2c, $dp], ['remote_ip:']), 'ho related item field empty'); +ok(acl_matches([$dip2c, $dp], ['!type:']), 'ho related item field not empty'); +is(acl_matches([$dip2c, $dp], ['foobar:xyz']), 0, 'ho unknown property'); done_testing;