From 802867a5d01cae46b52c7b2e18c5345f1a3c9e08 Mon Sep 17 00:00:00 2001 From: Oliver Gorwits Date: Tue, 14 Feb 2012 22:57:17 +0000 Subject: [PATCH] improve efficiency of general case device ports list --- Netdisco/lib/Netdisco/DB/Result/ActiveNode.pm | 148 ++++++++++++++++++ Netdisco/lib/Netdisco/DB/Result/DevicePort.pm | 111 +++++-------- .../DB/Result/DevicePortVlanTagged.pm | 81 ++++++++++ .../lib/Netdisco/DB/ResultSet/ActiveNode.pm | 64 ++++++++ .../lib/Netdisco/DB/ResultSet/DevicePort.pm | 60 ++++++- Netdisco/lib/Netdisco/Web/Device.pm | 32 ++-- Netdisco/views/ajax/device/ports.tt | 13 +- 7 files changed, 415 insertions(+), 94 deletions(-) create mode 100644 Netdisco/lib/Netdisco/DB/Result/ActiveNode.pm create mode 100644 Netdisco/lib/Netdisco/DB/Result/DevicePortVlanTagged.pm create mode 100644 Netdisco/lib/Netdisco/DB/ResultSet/ActiveNode.pm diff --git a/Netdisco/lib/Netdisco/DB/Result/ActiveNode.pm b/Netdisco/lib/Netdisco/DB/Result/ActiveNode.pm new file mode 100644 index 00000000..caa85cf5 --- /dev/null +++ b/Netdisco/lib/Netdisco/DB/Result/ActiveNode.pm @@ -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 to which this Node entry was associated at the +time of discovery. + +The JOIN is of type LEFT, in case the C is no longer present in the +database but the relation is being used in C. + +=cut + +__PACKAGE__->belongs_to( device => 'Netdisco::DB::Result::Device', + { 'foreign.ip' => 'self.switch' }, { join_type => 'LEFT' } ); + +=head2 device_port + +Returns the single C to which this Node entry was associated at +the time of discovery. + +The JOIN is of type LEFT, in case the C is no longer present in the +database but the relation is being used in C. + +=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 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 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 field, accurate to the minute. + +The format is somewhat like ISO 8601 or RFC3339 but without the middle C +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 field, accurate to the minute. + +The format is somewhat like ISO 8601 or RFC3339 but without the middle C +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 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; diff --git a/Netdisco/lib/Netdisco/DB/Result/DevicePort.pm b/Netdisco/lib/Netdisco/DB/Result/DevicePort.pm index 7dac2286..68592e5c 100644 --- a/Netdisco/lib/Netdisco/DB/Result/DevicePort.pm +++ b/Netdisco/lib/Netdisco/DB/Result/DevicePort.pm @@ -77,11 +77,7 @@ __PACKAGE__->belongs_to( device => 'Netdisco::DB::Result::Device', 'ip'); =head2 nodes Returns the set of Nodes whose MAC addresses are associated with this Device -Port. - -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 and C tables -include independent C fields. +Port. See C to find only the active Nodes, instead. =over 4 @@ -91,11 +87,6 @@ Rows returned are sorted by the Node MAC address. =item * -The Node's related IP addresses (that is, entries from the C table) -will also be retrieved. - -=item * - The additional column C is a preformatted value for the Node's C 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.port' => 'self.port', }, + { join_type => 'LEFT' }, +); + +=head2 active_nodes + +Returns the set of I Nodes whose MAC addresses are associated with +this Device Port. See C to find all Nodes (active and inactive). + +=over 4 + +=item * + +Rows returned are sorted by the Node MAC address. + +=item * + +The additional column C is a preformatted value for the Node's +C field, which reads as "X days/weeks/months/years". + +=back + +=cut + +__PACKAGE__->has_many( active_nodes => 'Netdisco::DB::Result::ActiveNode', { - prefetch => 'ips', - order_by => 'me.mac', - '+select' => [ - \"replace(age(date_trunc('minute', - me.time_last + interval '30 second'))::text, 'mon', 'month')", - ], - '+as' => [ 'me.time_last_age' ], + 'foreign.switch' => 'self.ip', + 'foreign.port' => 'self.port', }, + { join_type => 'LEFT' }, ); =head2 neighbor_alias @@ -139,19 +150,16 @@ __PACKAGE__->belongs_to( neighbor_alias => 'Netdisco::DB::Result::DeviceIp', =head2 port_vlans_tagged Returns a set of rows from the C table relating to this -port, where the VLANs are all tagged. See also the C -relationship. +port, where the VLANs are all tagged. =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.port' => 'self.port', }, - { - where => { -not_bool => 'me.native' }, - } + { join_type => 'LEFT' }, ); =head2 tagged_vlans @@ -167,25 +175,6 @@ See also C. __PACKAGE__->many_to_many( tagged_vlans => 'port_vlans_tagged', 'vlan' ); -=head2 native_port_vlan - -Returns an entry from the C table relating to this port, -where the VLAN is not tagged. - -See also the C 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 Returns the C table entry matching this Port. You can then join on this @@ -224,32 +213,6 @@ sub neighbor { return eval { $row->neighbor_alias->device || undef }; } -=head2 native_vlan - -This is a convenience method to be used instead of the C -relationship described above. - -Whereas the C relation returns the entire row from the -C 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 ) This method can be used to evaluate whether a device port could be considered @@ -290,9 +253,19 @@ sub is_free { =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 modifier to C. + +=cut + +sub tagged_vlans_count { return (shift)->get_column('tagged_vlans_count') } + =head2 lastchange_stamp -Formatted version of the C field, accurate to the minute. +Formatted version of the C field, accurate to the minute. Enable +this column by applying the C modifier to C. The format is somewhat like ISO 8601 or RFC3339 but without the middle C between the date stamp and time stamp. That is: diff --git a/Netdisco/lib/Netdisco/DB/Result/DevicePortVlanTagged.pm b/Netdisco/lib/Netdisco/DB/Result/DevicePortVlanTagged.pm new file mode 100644 index 00000000..117b4800 --- /dev/null +++ b/Netdisco/lib/Netdisco/DB/Result/DevicePortVlanTagged.pm @@ -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 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 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 table describing this VLAN in +detail, typically in order that the C can be retrieved. + +=cut + +__PACKAGE__->belongs_to( vlan => 'Netdisco::DB::Result::DeviceVlan', { + 'foreign.ip' => 'self.ip', 'foreign.vlan' => 'self.vlan', +}); + +1; diff --git a/Netdisco/lib/Netdisco/DB/ResultSet/ActiveNode.pm b/Netdisco/lib/Netdisco/DB/ResultSet/ActiveNode.pm new file mode 100644 index 00000000..53675347 --- /dev/null +++ b/Netdisco/lib/Netdisco/DB/ResultSet/ActiveNode.pm @@ -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, this returns a ResultSet of matching rows from the Node +table. + +=over 4 + +=item * + +The C parameter must be a hashref containing a key C with +the value to search for. + +=item * + +Results are ordered by time last seen. + +=item * + +Additional columns C and C provide +preformatted timestamps of the C and C fields. + +=item * + +A JOIN is performed on the Device table and the Device C column +prefetched. + +=back + +To limit results only to active nodes, set C<< {active => 1} >> in C. + +=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; diff --git a/Netdisco/lib/Netdisco/DB/ResultSet/DevicePort.pm b/Netdisco/lib/Netdisco/DB/ResultSet/DevicePort.pm index 524c89e7..345b7027 100644 --- a/Netdisco/lib/Netdisco/DB/ResultSet/DevicePort.pm +++ b/Netdisco/lib/Netdisco/DB/ResultSet/DevicePort.pm @@ -29,7 +29,7 @@ sub with_times { ->search({}, { '+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')", ], '+as' => [qw/ lastchange_stamp /], @@ -37,6 +37,64 @@ sub with_times { }); } +=head2 with_node_age + +This is a modifier for any C (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. + +=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 (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? ) my $set = $rs->search_by_mac({mac => '00:11:22:33:44:55'}); diff --git a/Netdisco/lib/Netdisco/Web/Device.pm b/Netdisco/lib/Netdisco/Web/Device.pm index 08ad879c..8f295ebc 100644 --- a/Netdisco/lib/Netdisco/Web/Device.pm +++ b/Netdisco/lib/Netdisco/Web/Device.pm @@ -75,9 +75,6 @@ ajax '/ajax/content/device/ports' => sub { my $set = schema('netdisco')->resultset('DevicePort') ->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 my $q = param('f'); if ($q) { @@ -96,21 +93,21 @@ ajax '/ajax/content/device/ports' => sub { } } - # retrieve related data for additonal table columns, if asked for - $set = $set->search_rs({}, {prefetch => {nodes => 'ips'}}) - if param('c_connected'); - $set = $set->search_rs({}, {prefetch => {port_vlans_tagged => 'vlan'}}) - if param('c_vmember'); + # make sure query asks for formatted timestamps when needed + $set = $set->with_times if param('c_lastchange'); - # if active or not, control the join to Node table - if (param('n_archived')) { - $set = $set->search_rs({ - -or => [{-bool => 'nodes.active'}, {-not_bool => 'nodes.active'}] - }); - } - else { - $set = $set->search_rs({-bool => 'nodes.active'}); - } + # get number of vlans on the port to control whether to list them or not + $set = $set->with_vlan_count if param('c_vmember'); + + # retrieve active/all connected nodes, and device, if asked for + my $nodes_name = (param('n_archived') ? 'nodes' : 'active_nodes'); + $set = $set->search_rs({}, { + prefetch => [{$nodes_name => 'ips'}, {neighbor_alias => 'device'}], + }) 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 # 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'); template 'ajax/device/ports.tt', { results => $results, + nodes => $nodes_name, }, { layout => undef }; }; diff --git a/Netdisco/views/ajax/device/ports.tt b/Netdisco/views/ajax/device/ports.tt index e47e9073..8dcab443 100644 --- a/Netdisco/views/ajax/device/ports.tt +++ b/Netdisco/views/ajax/device/ports.tt @@ -63,22 +63,21 @@ [% END %] [% IF params.c_vmember %] - [%# this is really ugly because for some reason - I could not get size/max to work on row.tagged_vlans.all %] + [% IF row.tagged_vlans_count %] [% SET output = '' %] - [% SET count = 0 %] [% FOREACH vlan IN row.tagged_vlans %] [% SET output = output _ '' _ vlan.vlan _ '' %] [% SET output = output _ ', ' IF NOT loop.last %] - [% SET count = count + 1 %] [% END %] - [% IF count > 10 %] - [% SET output = '
(' _ count _ ')
' + [% IF row.tagged_vlans_count > 10 %] + [% SET output = '
(' _ row.tagged_vlans_count + _ ')
' _ 'Show VLANs
' _ output %] [% SET output = output _ '
' %] [% END %] [% output %] + [% END %] [% END %] [% IF params.c_connected %] @@ -94,7 +93,7 @@
  ([% row.remote_type %]) / ([% row.remote_id %]) [% END %] [% END %] - [% FOREACH node IN row.nodes %] + [% FOREACH node IN row.$nodes %] [% '
' IF row.remote_ip OR NOT loop.first %] [% 'a  ' IF NOT node.active %] [% node.mac %]