1use utf8; 2package App::Netdisco::DB::Result::Device; 3 4use strict; 5use warnings; 6 7use NetAddr::IP::Lite ':lower'; 8use App::Netdisco::Util::DNS 'hostname_from_ip'; 9 10use overload '""' => sub { shift->ip }, fallback => 1; 11 12use base 'App::Netdisco::DB::Result'; 13__PACKAGE__->table("device"); 14__PACKAGE__->add_columns( 15 "ip", 16 { data_type => "inet", is_nullable => 0 }, 17 "creation", 18 { 19 data_type => "timestamp", 20 default_value => \"current_timestamp", 21 is_nullable => 1, 22 original => { default_value => \"now()" }, 23 }, 24 "dns", 25 { data_type => "text", is_nullable => 1 }, 26 "description", 27 { data_type => "text", is_nullable => 1 }, 28 "uptime", 29 { data_type => "bigint", is_nullable => 1 }, 30 "contact", 31 { data_type => "text", is_nullable => 1 }, 32 "name", 33 { data_type => "text", is_nullable => 1 }, 34 "location", 35 { data_type => "text", is_nullable => 1 }, 36 "layers", 37 { data_type => "varchar", is_nullable => 1, size => 8 }, 38 "num_ports", 39 { data_type => "integer", is_serializable => 0, is_nullable => 1 }, 40 "mac", 41 { data_type => "macaddr", is_nullable => 1 }, 42 "serial", 43 { data_type => "text", is_nullable => 1 }, 44 "model", 45 { data_type => "text", is_nullable => 1 }, 46 "ps1_type", 47 { data_type => "text", is_nullable => 1 }, 48 "ps2_type", 49 { data_type => "text", is_nullable => 1 }, 50 "ps1_status", 51 { data_type => "text", is_nullable => 1 }, 52 "ps2_status", 53 { data_type => "text", is_nullable => 1 }, 54 "fan", 55 { data_type => "text", is_nullable => 1 }, 56 "slots", 57 { data_type => "integer", is_nullable => 1 }, 58 "vendor", 59 { data_type => "text", is_nullable => 1 }, 60 "os", 61 { data_type => "text", is_nullable => 1 }, 62 "os_ver", 63 { data_type => "text", is_nullable => 1 }, 64 "log", 65 { data_type => "text", is_nullable => 1 }, 66 "snmp_ver", 67 { data_type => "integer", is_nullable => 1 }, 68 "snmp_comm", 69 { data_type => "text", is_nullable => 1 }, 70 "snmp_class", 71 { data_type => "text", is_nullable => 1 }, 72 "snmp_engineid", 73 { data_type => "text", is_nullable => 1 }, 74 "vtp_domain", 75 { data_type => "text", is_nullable => 1 }, 76 "last_discover", 77 { data_type => "timestamp", is_nullable => 1 }, 78 "last_macsuck", 79 { data_type => "timestamp", is_nullable => 1 }, 80 "last_arpnip", 81 { data_type => "timestamp", is_nullable => 1 }, 82); 83__PACKAGE__->set_primary_key("ip"); 84 85 86=head1 RELATIONSHIPS 87 88=head2 device_ips 89 90Returns rows from the C<device_ip> table which relate to this Device. That is, 91all the interface IP aliases configured on the Device. 92 93=cut 94 95__PACKAGE__->has_many( device_ips => 'App::Netdisco::DB::Result::DeviceIp', 'ip' ); 96 97=head2 device_ips_by_address_or_name 98 99Returns rows from the C<device_ip> table which relate to this Device. That is, 100all the interface IP aliases configured on the Device. However you probably 101want to use the C<device_ips_with_address_or_name> ResultSet method instead, 102so you can pass the MAC address part. 103 104=cut 105 106__PACKAGE__->has_many( device_ips_by_address_or_name => 'App::Netdisco::DB::Result::DeviceIp', 107 sub { 108 my $args = shift; 109 return { 110 "$args->{foreign_alias}.ip" => { -ident => "$args->{self_alias}.ip" }, 111 -or => [ 112 "$args->{foreign_alias}.dns" => { 'ilike', \'?' }, 113 "$args->{foreign_alias}.alias" => { '<<=', \'?' }, 114 "$args->{foreign_alias}.alias::text" => { 'ilike', \'?' }, 115 ], 116 }; 117 }, 118 { cascade_copy => 0, cascade_update => 0, cascade_delete => 0 } 119); 120 121=head2 vlans 122 123Returns the C<device_vlan> entries for this Device. That is, the list of VLANs 124configured on or known by this Device. 125 126=cut 127 128__PACKAGE__->has_many( vlans => 'App::Netdisco::DB::Result::DeviceVlan', 'ip' ); 129 130=head2 ports 131 132Returns the set of ports on this Device. 133 134=cut 135 136__PACKAGE__->has_many( ports => 'App::Netdisco::DB::Result::DevicePort', 'ip' ); 137 138=head2 ports_by_mac 139 140Returns the set of ports on this Device, filtered by MAC. However you probably 141want to use the C<ports_with_mac> ResultSet method instead, so you can pass the 142MAC address part. 143 144=cut 145 146__PACKAGE__->has_many( ports_by_mac => 'App::Netdisco::DB::Result::DevicePort', 147 sub { 148 my $args = shift; 149 return { 150 "$args->{foreign_alias}.ip" => { -ident => "$args->{self_alias}.ip" }, 151 "$args->{foreign_alias}.mac::text" => { 'ilike', \'?' }, 152 }; 153 }, 154 { cascade_copy => 0, cascade_update => 0, cascade_delete => 0 } 155); 156 157=head2 modules 158 159Returns the set chassis modules on this Device. 160 161=cut 162 163__PACKAGE__->has_many( modules => 'App::Netdisco::DB::Result::DeviceModule', 'ip' ); 164 165=head2 power_modules 166 167Returns the set of power modules on this Device. 168 169=cut 170 171__PACKAGE__->has_many( power_modules => 'App::Netdisco::DB::Result::DevicePower', 'ip' ); 172 173=head2 port_vlans 174 175Returns the set of VLANs known to be configured on Ports on this Device, 176either tagged or untagged. 177 178The JOIN is of type "RIGHT" meaning that the results are constrained to VLANs 179only on Ports on this Device. 180 181=cut 182 183__PACKAGE__->has_many( 184 port_vlans => 'App::Netdisco::DB::Result::DevicePortVlan', 185 'ip', { join_type => 'RIGHT' } 186); 187 188=head2 port_vlans_filter 189 190A JOIN condition which can be used to filter a set of Devices to those known 191carrying a given VLAN on its ports. Uses an INNER JOIN to achieve this. 192 193=cut 194 195__PACKAGE__->has_many( 196 port_vlans_filter => 'App::Netdisco::DB::Result::DevicePortVlan', 197 'ip', { join_type => 'INNER' } 198); 199 200# helper which assumes we've just RIGHT JOINed to Vlans table 201sub vlan { return (shift)->vlans->first } 202 203=head2 wireless_ports 204 205Returns the set of wireless IDs known to be configured on Ports on this 206Device. 207 208=cut 209 210__PACKAGE__->has_many( 211 wireless_ports => 'App::Netdisco::DB::Result::DevicePortWireless', 212 'ip', { join_type => 'RIGHT' } 213); 214 215=head2 ssids 216 217Returns the set of SSIDs known to be configured on Ports on this Device. 218 219=cut 220 221__PACKAGE__->has_many( 222 ssids => 'App::Netdisco::DB::Result::DevicePortSsid', 223 'ip', { join_type => 'RIGHT' } 224); 225 226=head2 properties_ports 227 228Returns the set of ports known to have recorded properties 229 230=cut 231 232__PACKAGE__->has_many( 233 properties_ports => 'App::Netdisco::DB::Result::DevicePortProperties', 234 'ip', { join_type => 'RIGHT' } 235); 236 237=head2 powered_ports 238 239Returns the set of ports known to have PoE capability 240 241=cut 242 243__PACKAGE__->has_many( 244 powered_ports => 'App::Netdisco::DB::Result::DevicePortPower', 245 'ip', { join_type => 'RIGHT' } 246); 247 248=head2 community 249 250Returns the row from the community string table, if one exists. 251 252=cut 253 254__PACKAGE__->might_have( 255 community => 'App::Netdisco::DB::Result::Community', 'ip'); 256 257=head2 throughput 258 259Returns a sum of speeds on all ports on the device. 260 261=cut 262 263__PACKAGE__->has_one( 264 throughput => 'App::Netdisco::DB::Result::Virtual::DevicePortSpeed', 'ip'); 265 266=head1 ADDITIONAL METHODS 267 268=head2 is_pseudo 269 270Returns true if the vendor of the device is "netdisco". 271 272=cut 273 274sub is_pseudo { 275 my $device = shift; 276 return (defined $device->vendor and $device->vendor eq 'netdisco'); 277} 278 279=head2 has_layer( $number ) 280 281Returns true if the device provided sysServices and supports the given layer. 282 283=cut 284 285sub has_layer { 286 my ($device, $layer) = @_; 287 return unless $layer and $layer =~ m/^[1-7]$/; 288 return ($device->layers and (substr($device->layers, (8-$layer), 1) == 1)); 289} 290 291=head2 renumber( $new_ip ) 292 293Will update this device and all related database records to use the new IP 294C<$new_ip>. Returns C<undef> if $new_ip seems invalid, otherwise returns the 295Device row object. 296 297=cut 298 299sub renumber { 300 my ($device, $ip) = @_; 301 my $schema = $device->result_source->schema; 302 303 my $new_addr = NetAddr::IP::Lite->new($ip) 304 or return; 305 306 my $old_ip = $device->ip; 307 my $new_ip = $new_addr->addr; 308 309 return 310 if $new_ip eq '0.0.0.0' 311 or $new_ip eq '127.0.0.1'; 312 313 # Community is not included as SNMP::test_connection will take care of it 314 foreach my $set (qw/ 315 DeviceIp 316 DeviceModule 317 DevicePort 318 DevicePortLog 319 DevicePortPower 320 DevicePortProperties 321 DevicePortSsid 322 DevicePortVlan 323 DevicePortWireless 324 DevicePower 325 DeviceVlan 326 /) { 327 $schema->resultset($set) 328 ->search({ip => $old_ip}) 329 ->update({ip => $new_ip}); 330 } 331 332 $schema->resultset('DeviceSkip') 333 ->search({device => $new_ip})->delete; 334 $schema->resultset('DeviceSkip') 335 ->search({device => $old_ip}) 336 ->update({device => $new_ip}); 337 338 $schema->resultset('DevicePort') 339 ->search({remote_ip => $old_ip}) 340 ->update({remote_ip => $new_ip}); 341 342 $schema->resultset('Node') 343 ->search({switch => $old_ip}) 344 ->update({switch => $new_ip}); 345 346 $schema->resultset('Topology') 347 ->search({dev1 => $old_ip}) 348 ->update({dev1 => $new_ip}); 349 350 $schema->resultset('Topology') 351 ->search({dev2 => $old_ip}) 352 ->update({dev2 => $new_ip}); 353 354 $device->update({ 355 ip => $new_ip, 356 dns => hostname_from_ip($new_ip), 357 }); 358 359 return $device; 360} 361 362=head1 ADDITIONAL COLUMNS 363 364=head2 oui 365 366Returns the first half of the device MAC address. 367 368=cut 369 370sub oui { return substr( ((shift)->mac || ''), 0, 8 ) } 371 372=head2 port_count 373 374Returns the number of ports on this device. Enable this 375column by applying the C<with_port_count()> modifier to C<search()>. 376 377=cut 378 379sub port_count { return (shift)->get_column('port_count') } 380 381 382=head2 uptime_age 383 384Formatted version of the C<uptime> field. 385 386The format is in "X days/months/years" style, similar to: 387 388 1 year 4 months 05:46:00 389 390=cut 391 392sub uptime_age { return (shift)->get_column('uptime_age') } 393 394=head2 first_seen_stamp 395 396Formatted version of the C<creation> field, accurate to the minute. 397 398The format is somewhat like ISO 8601 or RFC3339 but without the middle C<T> 399between the date stamp and time stamp. That is: 400 401 2012-02-06 12:49 402 403=cut 404 405sub first_seen_stamp { return (shift)->get_column('first_seen_stamp') } 406 407=head2 last_discover_stamp 408 409Formatted version of the C<last_discover> field, accurate to the minute. 410 411The format is somewhat like ISO 8601 or RFC3339 but without the middle C<T> 412between the date stamp and time stamp. That is: 413 414 2012-02-06 12:49 415 416=cut 417 418sub last_discover_stamp { return (shift)->get_column('last_discover_stamp') } 419 420=head2 last_macsuck_stamp 421 422Formatted version of the C<last_macsuck> field, accurate to the minute. 423 424The format is somewhat like ISO 8601 or RFC3339 but without the middle C<T> 425between the date stamp and time stamp. That is: 426 427 2012-02-06 12:49 428 429=cut 430 431sub last_macsuck_stamp { return (shift)->get_column('last_macsuck_stamp') } 432 433=head2 last_arpnip_stamp 434 435Formatted version of the C<last_arpnip> field, accurate to the minute. 436 437The format is somewhat like ISO 8601 or RFC3339 but without the middle C<T> 438between the date stamp and time stamp. That is: 439 440 2012-02-06 12:49 441 442=cut 443 444sub last_arpnip_stamp { return (shift)->get_column('last_arpnip_stamp') } 445 446=head2 since_last_discover 447 448Number of seconds which have elapsed since the value of C<last_discover>. 449 450=cut 451 452sub since_last_discover { return (shift)->get_column('since_last_discover') } 453 454=head2 since_last_macsuck 455 456Number of seconds which have elapsed since the value of C<last_macsuck>. 457 458=cut 459 460sub since_last_macsuck { return (shift)->get_column('since_last_macsuck') } 461 462=head2 since_last_arpnip 463 464Number of seconds which have elapsed since the value of C<last_arpnip>. 465 466=cut 467 468sub since_last_arpnip { return (shift)->get_column('since_last_arpnip') } 469 4701; 471