improve efficiency of general case device ports list

This commit is contained in:
Oliver Gorwits
2012-02-14 22:57:17 +00:00
parent c64b68c99c
commit 802867a5d0
7 changed files with 415 additions and 94 deletions

View File

@@ -0,0 +1,148 @@
use utf8;
package Netdisco::DB::Result::ActiveNode;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table("active_node");
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(
'SELECT * FROM node WHERE active'
);
__PACKAGE__->add_columns(
"mac",
{ data_type => "macaddr", is_nullable => 0 },
"switch",
{ data_type => "inet", is_nullable => 0 },
"port",
{ data_type => "text", is_nullable => 0 },
"active",
{ data_type => "boolean", is_nullable => 1 },
"oui",
{ data_type => "varchar", is_nullable => 1, size => 8 },
"time_first",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"time_recent",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"time_last",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
);
__PACKAGE__->set_primary_key("mac", "switch", "port");
=head1 RELATIONSHIPS
=head2 device
Returns the single C<device> to which this Node entry was associated at the
time of discovery.
The JOIN is of type LEFT, in case the C<device> is no longer present in the
database but the relation is being used in C<search()>.
=cut
__PACKAGE__->belongs_to( device => 'Netdisco::DB::Result::Device',
{ 'foreign.ip' => 'self.switch' }, { join_type => 'LEFT' } );
=head2 device_port
Returns the single C<device_port> to which this Node entry was associated at
the time of discovery.
The JOIN is of type LEFT, in case the C<device> is no longer present in the
database but the relation is being used in C<search()>.
=cut
# device port may have been deleted (reconfigured modules?) but node remains
__PACKAGE__->belongs_to( device_port => 'Netdisco::DB::Result::DevicePort',
{ 'foreign.ip' => 'self.switch', 'foreign.port' => 'self.port' },
{ join_type => 'LEFT' }
);
=head2 ips
Returns the set of C<node_ip> entries associated with this Node. That is, the
IP addresses which this MAC address was hosting at the time of discovery.
Note that the Active status of the returned IP entries will all be the same as
the current Node's.
=cut
__PACKAGE__->has_many( ips => 'Netdisco::DB::Result::NodeIp',
{ 'foreign.mac' => 'self.mac', 'foreign.active' => 'self.active' } );
=head2 oui
Returns the C<oui> table entry matching this Node. You can then join on this
relation and retrieve the Company name from the related table.
The JOIN is of type LEFT, in case the OUI table has not been populated.
=cut
__PACKAGE__->belongs_to( oui => 'Netdisco::DB::Result::Oui', 'oui',
{ join_type => 'LEFT' } );
=head1 ADDITIONAL COLUMNS
=head2 time_first_stamp
Formatted version of the C<time_first> field, accurate to the minute.
The format is somewhat like ISO 8601 or RFC3339 but without the middle C<T>
between the date stamp and time stamp. That is:
2012-02-06 12:49
=cut
sub time_first_stamp { return (shift)->get_column('time_first_stamp') }
=head2 time_last_stamp
Formatted version of the C<time_last> field, accurate to the minute.
The format is somewhat like ISO 8601 or RFC3339 but without the middle C<T>
between the date stamp and time stamp. That is:
2012-02-06 12:49
=cut
sub time_last_stamp { return (shift)->get_column('time_last_stamp') }
=head2 time_last_age
Formatted version of the C<time_last> field, accurate to the minute.
The format is in "X days/months/years" style, similar to:
1 year 4 months 05:46:00
=cut
sub time_last_age { return (shift)->get_column('time_last_age') }
1;

View File

@@ -77,11 +77,7 @@ __PACKAGE__->belongs_to( device => 'Netdisco::DB::Result::Device', 'ip');
=head2 nodes =head2 nodes
Returns the set of Nodes whose MAC addresses are associated with this Device Returns the set of Nodes whose MAC addresses are associated with this Device
Port. Port. See C<active_nodes()> to find only the active Nodes, instead.
Remember you can pass a filter to this method to find only active or inactive
nodes, but do take into account that both the C<node> and C<node_ip> tables
include independent C<active> fields.
=over 4 =over 4
@@ -91,11 +87,6 @@ Rows returned are sorted by the Node MAC address.
=item * =item *
The Node's related IP addresses (that is, entries from the C<node_ip> table)
will also be retrieved.
=item *
The additional column C<time_last_age> is a preformatted value for the Node's The additional column C<time_last_age> is a preformatted value for the Node's
C<time_last> field, which reads as "X days/weeks/months/years". C<time_last> field, which reads as "X days/weeks/months/years".
@@ -108,15 +99,35 @@ __PACKAGE__->has_many( nodes => 'Netdisco::DB::Result::Node',
'foreign.switch' => 'self.ip', 'foreign.switch' => 'self.ip',
'foreign.port' => 'self.port', 'foreign.port' => 'self.port',
}, },
{ join_type => 'LEFT' },
);
=head2 active_nodes
Returns the set of I<active> Nodes whose MAC addresses are associated with
this Device Port. See C<nodes()> to find all Nodes (active and inactive).
=over 4
=item *
Rows returned are sorted by the Node MAC address.
=item *
The additional column C<time_last_age> is a preformatted value for the Node's
C<time_last> field, which reads as "X days/weeks/months/years".
=back
=cut
__PACKAGE__->has_many( active_nodes => 'Netdisco::DB::Result::ActiveNode',
{ {
prefetch => 'ips', 'foreign.switch' => 'self.ip',
order_by => 'me.mac', 'foreign.port' => 'self.port',
'+select' => [
\"replace(age(date_trunc('minute',
me.time_last + interval '30 second'))::text, 'mon', 'month')",
],
'+as' => [ 'me.time_last_age' ],
}, },
{ join_type => 'LEFT' },
); );
=head2 neighbor_alias =head2 neighbor_alias
@@ -139,19 +150,16 @@ __PACKAGE__->belongs_to( neighbor_alias => 'Netdisco::DB::Result::DeviceIp',
=head2 port_vlans_tagged =head2 port_vlans_tagged
Returns a set of rows from the C<device_port_vlan> table relating to this Returns a set of rows from the C<device_port_vlan> table relating to this
port, where the VLANs are all tagged. See also the C<native_port_vlan> port, where the VLANs are all tagged.
relationship.
=cut =cut
__PACKAGE__->has_many( port_vlans_tagged => 'Netdisco::DB::Result::DevicePortVlan', __PACKAGE__->has_many( port_vlans_tagged => 'Netdisco::DB::Result::DevicePortVlanTagged',
{ {
'foreign.ip' => 'self.ip', 'foreign.ip' => 'self.ip',
'foreign.port' => 'self.port', 'foreign.port' => 'self.port',
}, },
{ { join_type => 'LEFT' },
where => { -not_bool => 'me.native' },
}
); );
=head2 tagged_vlans =head2 tagged_vlans
@@ -167,25 +175,6 @@ See also C<tagged_vlans_count>.
__PACKAGE__->many_to_many( tagged_vlans => 'port_vlans_tagged', 'vlan' ); __PACKAGE__->many_to_many( tagged_vlans => 'port_vlans_tagged', 'vlan' );
=head2 native_port_vlan
Returns an entry from the C<device_port_vlan> table relating to this port,
where the VLAN is not tagged.
See also the C<native_vlan> helper method.
=cut
__PACKAGE__->might_have( native_port_vlan => 'Netdisco::DB::Result::DevicePortVlan',
{
'foreign.ip' => 'self.ip',
'foreign.port' => 'self.port',
},
{
where => { -bool => 'me.native' },
}
);
=head2 oui =head2 oui
Returns the C<oui> table entry matching this Port. You can then join on this Returns the C<oui> table entry matching this Port. You can then join on this
@@ -224,32 +213,6 @@ sub neighbor {
return eval { $row->neighbor_alias->device || undef }; return eval { $row->neighbor_alias->device || undef };
} }
=head2 native_vlan
This is a convenience method to be used instead of the C<native_port_vlan>
relationship described above.
Whereas the C<native_port_vlan> relation returns the entire row from the
C<device_port_vlan> table, this helper returns the VLAN number itself from
that row - probably the thing you actually want in the end.
=cut
sub native_vlan {
my $row = shift;
return eval { $row->native_port_vlan->vlan || undef };
};
=head2 tagged_vlans_count
Returns the number of tagged VLANs active on this device port.
=cut
sub tagged_vlans_count {
return (shift)->tagged_vlans->count;
}
=head2 is_free( $quantity, $unit ) =head2 is_free( $quantity, $unit )
This method can be used to evaluate whether a device port could be considered This method can be used to evaluate whether a device port could be considered
@@ -290,9 +253,19 @@ sub is_free {
=head1 ADDITIONAL COLUMNS =head1 ADDITIONAL COLUMNS
=head2 tagged_vlans_count
Returns the number of tagged VLANs active on this device port. Enable this
column by applying the C<with_vlan_count()> modifier to C<search()>.
=cut
sub tagged_vlans_count { return (shift)->get_column('tagged_vlans_count') }
=head2 lastchange_stamp =head2 lastchange_stamp
Formatted version of the C<lastchange> field, accurate to the minute. Formatted version of the C<lastchange> field, accurate to the minute. Enable
this column by applying the C<with_vlan_count()> modifier to C<search()>.
The format is somewhat like ISO 8601 or RFC3339 but without the middle C<T> The format is somewhat like ISO 8601 or RFC3339 but without the middle C<T>
between the date stamp and time stamp. That is: between the date stamp and time stamp. That is:

View File

@@ -0,0 +1,81 @@
use utf8;
package Netdisco::DB::Result::DevicePortVlanTagged;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table("device_port_vlan_tagged");
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(
'SELECT * FROM device_port_vlan WHERE NOT native'
);
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"port",
{ data_type => "text", is_nullable => 0 },
"vlan",
{ data_type => "integer", is_nullable => 0 },
"native",
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"last_discover",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
);
__PACKAGE__->set_primary_key("ip", "port", "vlan");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:/3KLjJ3D18pGaPEaw9EU5w
=head1 RELATIONSHIPS
=head2 device
Returns the entry from the C<device> table which hosts the Port on which this
VLAN is configured.
=cut
__PACKAGE__->belongs_to( device => 'Netdisco::DB::Result::Device', 'ip' );
=head2 port
Returns the entry from the C<port> table on which this VLAN is configured.
=cut
__PACKAGE__->belongs_to( port => 'Netdisco::DB::Result::DevicePort', {
'foreign.ip' => 'self.ip', 'foreign.port' => 'self.port',
});
=head2 vlan
Returns the entry from the C<device_vlan> table describing this VLAN in
detail, typically in order that the C<name> can be retrieved.
=cut
__PACKAGE__->belongs_to( vlan => 'Netdisco::DB::Result::DeviceVlan', {
'foreign.ip' => 'self.ip', 'foreign.vlan' => 'self.vlan',
});
1;

View File

@@ -0,0 +1,64 @@
package Netdisco::DB::ResultSet::ActiveNode;
use base 'DBIx::Class::ResultSet';
use strict;
use warnings FATAL => 'all';
=head1 search_by_mac( \%cond, \%attrs? )
my $set = $rs->search_by_mac({mac => '00:11:22:33:44:55', active => 1});
Like C<search()>, this returns a ResultSet of matching rows from the Node
table.
=over 4
=item *
The C<cond> parameter must be a hashref containing a key C<mac> with
the value to search for.
=item *
Results are ordered by time last seen.
=item *
Additional columns C<time_first_stamp> and C<time_last_stamp> provide
preformatted timestamps of the C<time_first> and C<time_last> fields.
=item *
A JOIN is performed on the Device table and the Device C<dns> column
prefetched.
=back
To limit results only to active nodes, set C<< {active => 1} >> in C<cond>.
=cut
sub search_by_mac {
my ($rs, $cond, $attrs) = @_;
die "mac address required for search_by_mac\n"
if ref {} ne ref $cond or !exists $cond->{mac};
$cond->{'me.mac'} = delete $cond->{mac};
$attrs ||= {};
return $rs
->search_rs({}, {
order_by => {'-desc' => 'time_last'},
'+columns' => [qw/ device.dns /],
'+select' => [
\"to_char(time_first, 'YYYY-MM-DD HH24:MI')",
\"to_char(time_last, 'YYYY-MM-DD HH24:MI')",
],
'+as' => [qw/ time_first_stamp time_last_stamp /],
join => 'device',
})
->search($cond, $attrs);
}
1;

View File

@@ -29,7 +29,7 @@ sub with_times {
->search({}, ->search({},
{ {
'+select' => [ '+select' => [
\"to_char(last_discover - (uptime - lastchange) / 100 * interval '1 second', \"to_char(device.last_discover - (device.uptime - lastchange) / 100 * interval '1 second',
'YYYY-MM-DD HH24:MI:SS')", 'YYYY-MM-DD HH24:MI:SS')",
], ],
'+as' => [qw/ lastchange_stamp /], '+as' => [qw/ lastchange_stamp /],
@@ -37,6 +37,64 @@ sub with_times {
}); });
} }
=head2 with_node_age
This is a modifier for any C<search()> (including the helpers below) which
will add the following additional synthesized columns to the result set:
=over 4
=item $nodes.time_last_age
=back
You can pass in the table alias for the Nodes relation, which defaults to
C<nodes>.
=cut
sub with_node_age {
my ($rs, $alias) = @_;
$alias ||= 'nodes';
return $rs
->search_rs({},
{
'+select' =>
[\"replace(age(date_trunc('minute', $alias.time_last + interval '30 second'))::text, 'mon', 'month')"],
'+as' => [ "$alias.time_last_age" ],
});
}
=head2 with_vlan_count
This is a modifier for any C<search()> (including the helpers below) which
will add the following additional synthesized columns to the result set:
=over 4
=item tagged_vlans_count
=back
=cut
sub with_vlan_count {
my ($rs, $cond, $attrs) = @_;
$cond ||= {};
$attrs ||= {};
return $rs
->search_rs($cond, $attrs)
->search({},
{
'+select' => [ { count => 'port_vlans_tagged.vlan' } ],
'+as' => [qw/ tagged_vlans_count /],
join => 'port_vlans_tagged',
distinct => 1,
});
}
=head2 search_by_mac( \%cond, \%attrs? ) =head2 search_by_mac( \%cond, \%attrs? )
my $set = $rs->search_by_mac({mac => '00:11:22:33:44:55'}); my $set = $rs->search_by_mac({mac => '00:11:22:33:44:55'});

View File

@@ -75,9 +75,6 @@ ajax '/ajax/content/device/ports' => sub {
my $set = schema('netdisco')->resultset('DevicePort') my $set = schema('netdisco')->resultset('DevicePort')
->search_by_ip({ip => $ip}); ->search_by_ip({ip => $ip});
# make sure query asks for formatted timestamps when needed
$set = $set->with_times if param('c_lastchange');
# refine by ports if requested # refine by ports if requested
my $q = param('f'); my $q = param('f');
if ($q) { if ($q) {
@@ -96,21 +93,21 @@ ajax '/ajax/content/device/ports' => sub {
} }
} }
# retrieve related data for additonal table columns, if asked for # make sure query asks for formatted timestamps when needed
$set = $set->search_rs({}, {prefetch => {nodes => 'ips'}}) $set = $set->with_times if param('c_lastchange');
if param('c_connected');
$set = $set->search_rs({}, {prefetch => {port_vlans_tagged => 'vlan'}})
if param('c_vmember');
# if active or not, control the join to Node table # get number of vlans on the port to control whether to list them or not
if (param('n_archived')) { $set = $set->with_vlan_count if param('c_vmember');
$set = $set->search_rs({
-or => [{-bool => 'nodes.active'}, {-not_bool => 'nodes.active'}] # retrieve active/all connected nodes, and device, if asked for
}); my $nodes_name = (param('n_archived') ? 'nodes' : 'active_nodes');
} $set = $set->search_rs({}, {
else { prefetch => [{$nodes_name => 'ips'}, {neighbor_alias => 'device'}],
$set = $set->search_rs({-bool => 'nodes.active'}); }) if param('c_connected');
}
# add constructed node age col if requested (and showing connected)
$set = $set->with_node_age($nodes_name)
if param('c_connected') and param('n_age');
# sort, and filter by free ports # sort, and filter by free ports
# the filter could be in the template but here allows a 'no records' msg # the filter could be in the template but here allows a 'no records' msg
@@ -123,6 +120,7 @@ ajax '/ajax/content/device/ports' => sub {
content_type('text/html'); content_type('text/html');
template 'ajax/device/ports.tt', { template 'ajax/device/ports.tt', {
results => $results, results => $results,
nodes => $nodes_name,
}, { layout => undef }; }, { layout => undef };
}; };

View File

@@ -63,22 +63,21 @@
[% END %] [% END %]
[% IF params.c_vmember %] [% IF params.c_vmember %]
<td> <td>
[%# this is really ugly because for some reason [% IF row.tagged_vlans_count %]
I could not get size/max to work on row.tagged_vlans.all %]
[% SET output = '' %] [% SET output = '' %]
[% SET count = 0 %]
[% FOREACH vlan IN row.tagged_vlans %] [% FOREACH vlan IN row.tagged_vlans %]
[% SET output = output _ [% SET output = output _
'<a href="' _ uri_for('/search') _ '?tab=vlan&q=' _ vlan.vlan _ '">' _ vlan.vlan _ '</a>' %] '<a href="' _ uri_for('/search') _ '?tab=vlan&q=' _ vlan.vlan _ '">' _ vlan.vlan _ '</a>' %]
[% SET output = output _ ', ' IF NOT loop.last %] [% SET output = output _ ', ' IF NOT loop.last %]
[% SET count = count + 1 %]
[% END %] [% END %]
[% IF count > 10 %] [% IF row.tagged_vlans_count > 10 %]
[% SET output = '<div class="vlan_total">(' _ count _ ')</div><a href="#" class="nd_linkcell nd_collapse_vlans">' [% SET output = '<div class="vlan_total">(' _ row.tagged_vlans_count
_ ')</div><a href="#" class="nd_linkcell nd_collapse_vlans">'
_ 'Show VLANs</a><div class="nd_collapse_pre_hidden">' _ output %] _ 'Show VLANs</a><div class="nd_collapse_pre_hidden">' _ output %]
[% SET output = output _ '</div>' %] [% SET output = output _ '</div>' %]
[% END %] [% END %]
[% output %] [% output %]
[% END %]
</td> </td>
[% END %] [% END %]
[% IF params.c_connected %] [% IF params.c_connected %]
@@ -94,7 +93,7 @@
<br/>&nbsp; ([% row.remote_type %]) / ([% row.remote_id %])</a> <br/>&nbsp; ([% row.remote_type %]) / ([% row.remote_id %])</a>
[% END %] [% END %]
[% END %] [% END %]
[% FOREACH node IN row.nodes %] [% FOREACH node IN row.$nodes %]
[% '<br/>' IF row.remote_ip OR NOT loop.first %] [% '<br/>' IF row.remote_ip OR NOT loop.first %]
[% '<span class="label warning">a</span> &nbsp;' IF NOT node.active %] [% '<span class="label warning">a</span> &nbsp;' IF NOT node.active %]
<a href="[% uri_for('/search') %]?tab=node&q=[% node.mac | uri %]">[% node.mac %]</a> <a href="[% uri_for('/search') %]?tab=node&q=[% node.mac | uri %]">[% node.mac %]</a>