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