Compare commits

...

10 Commits

Author SHA1 Message Date
Oliver Gorwits
1d5e5ad8cb release 2.071001 2023-12-07 15:49:00 +00:00
Oliver Gorwits
12c4b22ea2 update vlansneverconfigured to skip vlan 1 2023-12-07 15:47:45 +00:00
Oliver Gorwits
ccd2de0651 better approach to HTML entity encoding in custom report searchable fields 2023-12-07 15:45:50 +00:00
Oliver Gorwits
9128c0d50d release 2.071000 2023-12-07 07:57:51 +00:00
Oliver Gorwits
bc47b54f67 improve searchable generic report fields (#1133)
* implement links on all v4/v6/mac found in _searchable fields

* word boundaries on matching
2023-12-07 07:49:18 +00:00
Oliver Gorwits
b85ca1140f provide default empty database setting 2023-12-06 21:24:54 +00:00
Oliver Gorwits
a91e518f16 new displayname option for default database 2023-12-06 21:06:47 +00:00
Oliver Gorwits
a068960b51 change system custom reports to use line wrapping arrays with a window function 2023-12-06 07:20:58 +00:00
Oliver Gorwits
8397eabe50 custom reports returning array columns will be split over lines 2023-12-05 21:32:41 +00:00
Oliver Gorwits
2afa56dde9 add device port external links support 2023-12-05 21:11:43 +00:00
13 changed files with 210 additions and 76 deletions

View File

@@ -79,6 +79,7 @@ Module::Build->new(
'Plack::Middleware::ReverseProxy' => '0.15',
'Pod::Usage' => 0,
'Regexp::Common' => 2017060201,
'Regexp::Common::net::CIDR' => 0,
'Role::Tiny' => '1.002005',
'Scope::Guard' => 0,
'Sereal' => '0',

32
Changes
View File

@@ -1,3 +1,35 @@
2.071001 - 2023-12-07
[BUG FIXES]
* update vlansneverconfigured report to skip vlan 1
* better approach to HTML entity encoding in custom report searchable fields
2.071000 - 2023-12-07
[NEW FEATURES]
* device port external links support
* new preset fields and custom fields support for device external links
* custom reports returning array columns will be split over lines
* default database in tenancies can have a friendly name
* #1133 improve searchable generic report fields
[ENHANCEMENTS]
* tidied up the report menus and report names
* #830 subnets report can show all subnets to start with
* #920 unused VLANs report
* #999 device vlan count report
* #1018 VLANs with Multiple Names report
* #1022 VLANs Known but Not Configured report
* #1023 ports with most vlans report
* #1052 duplicate private networks report
[BUG FIXES]
* do not reverse the external links for IPs and Devices
2.070003 - 2023-11-24
[BUG FIXES]

View File

@@ -578,6 +578,8 @@ share/views/ajax/report/subnets.tt
share/views/ajax/report/subnets_csv.tt
share/views/ajax/report/vlaninventory.tt
share/views/ajax/report/vlaninventory_csv.tt
share/views/ajax/report/vlanmultiplenames.tt
share/views/ajax/report/vlanmultiplenames_csv.tt
share/views/ajax/search/device.tt
share/views/ajax/search/device_csv.tt
share/views/ajax/search/node_by_ip.tt

View File

@@ -93,6 +93,7 @@
"Plack::Middleware::ReverseProxy" : "0.15",
"Pod::Usage" : "0",
"Regexp::Common" : "2017060201",
"Regexp::Common::net::CIDR" : "0",
"Role::Tiny" : "1.002005",
"SNMP::Info" : "3.95",
"SQL::Abstract" : "1.85",
@@ -138,7 +139,7 @@
"provides" : {
"App::Netdisco" : {
"file" : "lib/App/Netdisco.pm",
"version" : "2.070003"
"version" : "2.071001"
},
"App::Netdisco::AnyEvent::Nbtstat" : {
"file" : "lib/App/Netdisco/AnyEvent/Nbtstat.pm"
@@ -961,6 +962,6 @@
"x_IRC" : "irc://irc.libera.chat/#netdisco",
"x_MailingList" : "https://lists.sourceforge.net/lists/listinfo/netdisco-users"
},
"version" : "2.070003",
"version" : "2.071001",
"x_serialization_backend" : "JSON::PP version 4.16"
}

View File

@@ -23,7 +23,7 @@ name: App-Netdisco
provides:
App::Netdisco:
file: lib/App/Netdisco.pm
version: '2.070003'
version: '2.071001'
App::Netdisco::AnyEvent::Nbtstat:
file: lib/App/Netdisco/AnyEvent/Nbtstat.pm
App::Netdisco::Backend::Job:
@@ -625,6 +625,7 @@ requires:
Plack::Middleware::ReverseProxy: '0.15'
Pod::Usage: '0'
Regexp::Common: '2017060201'
Regexp::Common::net::CIDR: '0'
Role::Tiny: '1.002005'
SNMP::Info: '3.95'
SQL::Abstract: '1.85'
@@ -663,5 +664,5 @@ resources:
homepage: http://netdisco.org/
license: http://opensource.org/licenses/BSD-3-Clause
repository: https://github.com/netdisco/netdisco
version: '2.070003'
version: '2.071001'
x_serialization_backend: 'CPAN::Meta::YAML version 0.018'

View File

@@ -4,7 +4,7 @@ use strict;
use warnings;
use 5.010_000;
our $VERSION = '2.070003';
our $VERSION = '2.071001';
use App::Netdisco::Configuration;
=head1 NAME

View File

@@ -247,7 +247,7 @@ hook after_error_render => sub { setting('layout' => 'main') };
tag => $_->{'tag'},
path => config->{'url_base'}->with("/t/$_->{tag}")->path } ) }
@{ setting('tenant_databases') },
{ tag => 'netdisco', displayname => 'Default' }
{ tag => 'netdisco', displayname => (setting('database')->{displayname} || 'Default') }
});
config->{'tenant_data'}->{'netdisco'}->{'path'}
= URI::Based->new((config->{path} eq '/') ? '' : config->{path})->path;

View File

@@ -9,6 +9,10 @@ use Path::Class 'file';
use Storable 'dclone';
use Safe;
use HTML::Entities 'encode_entities';
use Regexp::Common qw( RE_net_IPv4 RE_net_IPv6 RE_net_MAC RE_net_domain );
use Regexp::Common::net::CIDR ();
our ($config, @data);
foreach my $report (@{setting('reports')}) {
@@ -72,6 +76,42 @@ foreach my $report (@{setting('reports')}) {
my @results = ((-f $munger) ? $compartment->rdo( $munger ) : @data);
return if $@ or (0 == scalar @results);
# searchable field support..
my $recidr4 = $RE{net}{CIDR}{IPv4}{-keep}; #RE_net_CIDR_IPv4(-keep);
my $rev4 = RE_net_IPv4(-keep);
my $rev6 = RE_net_IPv6(-keep);
my $remac = RE_net_MAC(-keep);
foreach my $row (@results) {
foreach my $col (@column_order) {
next unless $column_config{$col}->{_searchable};
my $fields = (ref $row->{$col} ? $row->{$col} : [$row->{$col}]);
foreach my $f (@$fields) {
encode_entities($f);
$f =~ s!\b${recidr4}\b!'<a href="'.
uri_for('/search', {q => "$1/$2"})->path_query
.qq{">$1/$2</a>}!gex;
if (not $1 and not $2) {
$f =~ s!\b${rev4}\b!'<a href="'.
uri_for('/search', {q => $1})->path_query .qq{">$1</a>}!gex;
}
$f =~ s!\b${rev6}\b!'<a href="'.
uri_for('/search', {q => $1})->path_query .qq{">$1</a>}!gex;
$f =~ s!\b${remac}\b!'<a href="'.
uri_for('/search', {q => $1})->path_query .qq{">$1</a>}!gex;
$row->{$col} = $f if not ref $row->{$col};
}
}
}
if (request->is_ajax) {
template 'ajax/report/generic_report.tt',
{ results => \@results,

View File

@@ -16,6 +16,7 @@ logger_format: '[%P] %U %L %m'
include_paths: []
template_paths: []
site_local_files: false
database: {}
external_databases: []
tenant_databases: []
@@ -126,21 +127,22 @@ sidebar_defaults:
c_name: { label: 'Name', default: checked, idx: 7 }
c_tags: { label: 'Tags', default: null, idx: 8 }
c_speed_admin: { label: 'Speed (configured)', default: null, idx: 9 }
c_speed: { label: 'Speed (running)', default: null, idx: 10 }
c_duplex_admin: { label: 'Duplex (configured)', default: null, idx: 11 }
c_duplex: { label: 'Duplex (running)', default: null, idx: 12 }
c_error: { label: 'Error Message', default: null, idx: 13 }
c_mac: { label: 'Port MAC', default: null, idx: 14 }
c_mtu: { label: 'MTU', default: null, idx: 15 }
c_pvid: { label: 'Native VLAN', default: checked, idx: 16 }
c_vmember: { label: 'VLAN Membership', default: checked, idx: 17 }
c_power: { label: 'PoE', default: null, idx: 18 }
c_ssid: { label: 'SSID', default: null, idx: 19 }
c_nac_summary: { label: 'NAC/802.1X Status', default: null, idx: 20 }
c_nodes: { label: 'Connected Nodes', default: null, idx: 21 }
c_neighbors: { label: 'Connected Devices', default: checked, idx: 22 }
c_stp: { label: 'Spanning Tree', default: null, idx: 23 }
c_up: { label: 'Up/Down Status', default: null, idx: 24 }
c_links: { label: 'External Links', default: checked, idx: 10 }
c_speed: { label: 'Speed (running)', default: null, idx: 11 }
c_duplex_admin: { label: 'Duplex (configured)', default: null, idx: 12 }
c_duplex: { label: 'Duplex (running)', default: null, idx: 13 }
c_error: { label: 'Error Message', default: null, idx: 14 }
c_mac: { label: 'Port MAC', default: null, idx: 15 }
c_mtu: { label: 'MTU', default: null, idx: 16 }
c_pvid: { label: 'Native VLAN', default: checked, idx: 17 }
c_vmember: { label: 'VLAN Membership', default: checked, idx: 18 }
c_power: { label: 'PoE', default: null, idx: 19 }
c_ssid: { label: 'SSID', default: null, idx: 20 }
c_nac_summary: { label: 'NAC/802.1X Status', default: null, idx: 21 }
c_nodes: { label: 'Connected Nodes', default: null, idx: 22 }
c_neighbors: { label: 'Connected Devices', default: checked, idx: 23 }
c_stp: { label: 'Spanning Tree', default: null, idx: 24 }
c_up: { label: 'Up/Down Status', default: null, idx: 25 }
mac_format: { default: IEEE }
n_inventory: { label: 'Remote Inventory', default: checked, idx: 0 }
n_detailed_inventory: { label: 'Remote Advertisement', default: null, idx: 1 }
@@ -171,8 +173,8 @@ sidebar_defaults:
age_num: { default: 3 }
age_unit: { default: months }
device_port_col_idx_left: 8
device_port_col_idx_mid: 20
device_port_col_idx_right: 24
device_port_col_idx_mid: 21
device_port_col_idx_right: 25
jobqueue_refresh: 5
safe_password_store: true
reports: []
@@ -181,7 +183,7 @@ system_reports:
label: 'Blocked - Error-Disabled'
category: Port
columns:
- { ip: Device }
- { ip: Device, _searchable: true }
- { dns: DNS }
- { port: Port }
- { name: Description }
@@ -257,14 +259,14 @@ system_reports:
HAVING count(vlan) > COALESCE(NULLIF(?,''), '1') ::integer
ORDER BY vlans DESC, ip ASC, port ASC
- tag: duplicateprivatenetworks
category: Port
category: IP
label: 'Duplicate Private Networks'
columns:
- { subnet: 'Subnet', _searchable: true }
- { count: 'Instances' }
- { seen: 'Where Seen' }
- { seen: 'Where Seen', _searchable: true }
query: |
SELECT subnet, count(subnet), array_to_string(array_agg(host(alias)::text || ' on ' || host(ip)::text), ', ') AS seen
SELECT subnet, count(subnet), array_agg(host(alias)::text || ' on ' || host(ip)::text) AS seen
FROM device_ip
WHERE ip <> alias
AND (masklen(subnet) <> 32 AND masklen(subnet) <> 128)
@@ -282,64 +284,85 @@ system_reports:
columns:
- { ip: 'Device IP', _searchable: true }
- { vlans: 'VLAN List' }
bind_params: ['chunk_size']
query: |
SELECT ip, array_to_string(array_agg(DISTINCT vlan::integer ORDER BY vlan::integer ASC), ', ') AS vlans
FROM device_port_vlan dpv
WHERE native IS false
AND vlan <> 1
AND (
SELECT count(*) FROM device_port_vlan dpv2
WHERE dpv2.ip = dpv.ip
AND dpv2.vlan = dpv.vlan
AND native IS true
) = 0
GROUP BY ip
ORDER BY ip
SELECT ip, array_agg(vlans) AS vlans FROM (
SELECT ip, array_to_string(array_agg(vlan), ', ') AS vlans, (x / COALESCE(NULLIF(?,''), '20') ::integer) AS chunk FROM (
SELECT *, (row_number() over (partition by ip)) AS x FROM (
SELECT DISTINCT ip, vlan
FROM device_port_vlan dpv
WHERE native IS false
AND vlan <> 1
AND (
SELECT count(*) FROM device_port_vlan dpv2
WHERE dpv2.ip = dpv.ip
AND dpv2.vlan = dpv.vlan
AND native IS true
) = 0
ORDER BY ip, vlan)
) GROUP BY ip, chunk
) GROUP BY ip ORDER BY ip
- tag: vlansneverconfigured
category: VLAN
label: 'VLANs Known but Not Configured'
columns:
- { ip: 'Device IP', _searchable: true }
- { vlans: 'VLAN List' }
bind_params: ['chunk_size']
query: |
SELECT ip, array_to_string(array_agg(DISTINCT dv.vlan::integer ORDER BY dv.vlan::integer ASC), ', ') AS vlans
FROM device_vlan dv
WHERE NOT EXISTS (
SELECT FROM device_port_vlan dpv
WHERE dpv.ip = dv.ip
AND dpv.vlan = dv.vlan
)
AND vlan NOT IN (1002, 1003, 1004, 1005)
GROUP BY ip
ORDER BY ip
SELECT ip, array_agg(vlans) AS vlans FROM (
SELECT ip, array_to_string(array_agg(vlan), ', ') AS vlans, (x / COALESCE(NULLIF(?,''), '20') ::integer) AS chunk FROM (
SELECT *, (row_number() over (partition by ip)) AS x FROM (
SELECT DISTINCT ip, vlan
FROM device_vlan dv
WHERE vlan <> 1
AND NOT EXISTS (
SELECT FROM device_port_vlan dpv
WHERE dpv.ip = dv.ip
AND dpv.vlan = dv.vlan
)
AND vlan NOT IN (1002, 1003, 1004, 1005)
ORDER BY ip, vlan)
) GROUP BY ip, chunk
) GROUP BY ip ORDER BY ip
- tag: vlansunused
category: VLAN
label: 'VLANs No Longer Used'
columns:
- { ip: 'Device IP', _searchable: true }
- { vlans: 'VLAN List' }
bind_params: ['free']
bind_params: ['chunk_size', 'free']
query: |
SELECT dpv.ip, array_to_string(array_agg(DISTINCT dpv.vlan::integer ORDER BY dpv.vlan::integer ASC), ', ') AS vlans
FROM device_port_vlan dpv
WHERE dpv.native IS false
AND dpv.vlan <> 1
AND (
SELECT count(*) FROM device_port_vlan dpv2
LEFT JOIN device_port dp USING (ip, port)
LEFT JOIN device d USING (ip)
WHERE dpv2.ip = dpv.ip
AND dpv2.vlan = dpv.vlan
AND native IS true
SELECT ip, array_agg(vlans) AS vlans FROM (
SELECT ip, array_to_string(array_agg(vlan), ', ') AS vlans, (x / COALESCE(NULLIF(?,''), '20') ::integer) AS chunk FROM (
SELECT *, (row_number() over (partition by ip)) AS x FROM (
SELECT DISTINCT ip, vlan
FROM device_port_vlan dpv
WHERE dpv.native IS false
AND dpv.vlan <> 1
AND (
dp.up_admin = 'up'
OR age( LOCALTIMESTAMP,
to_timestamp( extract( epoch FROM d.last_discover ) - ( d.uptime - dp.lastchange ) /100 ) ::timestamp )
< COALESCE(NULLIF(?,''), '3 months') ::interval
)
) = 0
GROUP BY dpv.ip
ORDER BY dpv.ip
SELECT count(*) FROM device_port_vlan dpv2
LEFT JOIN device_port dp USING (ip, port)
LEFT JOIN device d USING (ip)
WHERE dpv2.ip = dpv.ip
AND dpv2.vlan = dpv.vlan
AND native IS true
AND (
dp.up_admin = 'up'
OR age( LOCALTIMESTAMP,
to_timestamp( extract( epoch FROM d.last_discover ) - ( d.uptime - dp.lastchange ) /100 ) ::timestamp )
< COALESCE(NULLIF(?,''), '3 months') ::interval
)
) = 0
ORDER BY dpv.ip, dpv.vlan)
) GROUP BY ip, chunk
) GROUP BY ip ORDER BY ip
- tag: devicevlancount
category: VLAN
label: 'VLAN Count per Device'

View File

@@ -1,3 +1,4 @@
[% PROCESS 'externallinks.tt' -%]
[% SET user_can_port_control = user_has_role('port_control', device) %]
<table id="dp-data-table" class="table table-bordered table-striped" width="100%" cellspacing="0">
<thead>
@@ -5,6 +6,7 @@
<th></th>
[% FOREACH item IN settings.port_columns %]
[% NEXT IF item.name == 'c_admin' %]
[% NEXT IF item.name == 'c_links' AND settings.external_links.device_port.empty %]
[% NEXT IF item.name == 'c_nodes' AND params.c_nodes AND params.c_neighbors %]
[% NEXT UNLESS params.${item.name} %]
[% SET th_class = '' %]
@@ -177,6 +179,15 @@
[% END %]
[% IF params.c_links AND settings.external_links.device_port.size > 0 %]
<td>
[% INCLUDE external_device_port_links
item = row
d = device
%]
</td>
[% END %]
[% IF params.c_speed_admin %]
<td>[% row.speed_admin | html_entity %]</td>
[% END %]

View File

@@ -10,13 +10,16 @@
[% FOREACH row IN results %]
<tr>
[% FOREACH col IN columns %]
[% IF column_options.$col._searchable %]
<td>
<a href="[% uri_for('/search') | none %]?q=[% row.item(col) | uri %]">[% row.item(col) | html_entity %]</a>
</td>
[% ELSE %]
<td>[% row.item(col) | html_entity %]</td>
<td>
[% FOREACH record IN row.item(col) %]
[% IF column_options.$col._searchable %]
[% record | none %]
[% ELSE %]
[% record | html_entity %]
[% END %]
[% '<br />' IF loop.size > 1 %]
[% END %]
</td>
[% END %]
</tr>
[% END %]

View File

@@ -2,6 +2,7 @@
<a href="[% link.url | evaltt %]" target="_blank">
<span class="label label-default"><i class="icon-external-link"></i> [% link.displayname | html_entity %]</span></a>
[% END %]
[% BLOCK external_mac_links %]
[% FOREACH link IN settings.external_links.node.reverse %]
[% NEXT UNLESS link.for_mac %]
@@ -11,6 +12,7 @@
<span class="nd_node-ext-link">&nbsp;</span>
[% END %]
[% END %]
[% BLOCK external_ip_links %]
[% FOREACH link IN settings.external_links.node %]
[% NEXT UNLESS link.for_ip %]
@@ -18,6 +20,7 @@
[% PROCESS external_link %]
[% END %]
[% END %]
[% BLOCK external_device_links %]
[% FOREACH link IN settings.external_links.device %]
[% device = item.ip FILTER uri %]
@@ -33,3 +36,20 @@
[% PROCESS external_link %]
[% END %]
[% END %]
[% BLOCK external_device_port_links %]
[% FOREACH link IN settings.external_links.device_port %]
[% device = d.ip FILTER uri %]
[% fqdn = d.dns FILTER uri %]
[% sysname = d.name FILTER uri %]
[% hostname = d.dns.remove(settings.domain_suffix) FILTER uri %]
[% port = item.port FILTER uri %]
[% mac = item.mac FILTER uri %]
[% ifindex = item.ifindex FILTER uri %]
[% FOREACH config IN settings._extra_device_port_cols %]
[% NEXT UNLESS config.field.match('^cf_') %]
[% ${config.field} = item.get_column(${config.field}) FILTER uri %]
[% END %]
[% PROCESS external_link %]
[% END %]
[% END %]

View File

@@ -176,7 +176,7 @@
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-home text-success"></i>
&nbsp;[% settings.tenant_data.$tenant.displayname || 'Default' | html_entity %]
&nbsp;[% (settings.tenant_data.$tenant.displayname || settings.database.displayname || 'Default') | html_entity %]
<b class="caret"></b>
</a>
<ul class="dropdown-menu">