register_worker({
phase
=>
'early'
,
title
=>
'prepare common data'
},
sub
{
my
(
$job
,
$workerconf
) =
@_
;
my
$device
=
$job
->device;
vars->{
'timestamp'
} = (
$job
->is_offline and
$job
->entered)
? (schema(
'netdisco'
)->storage->dbh->quote(
$job
->entered) .
'::timestamp'
)
:
'to_timestamp('
. (
join
'.'
, gettimeofday) .
')::timestamp'
;
vars->{
'fwtable'
} = {};
vars->{
'device_ports'
} = {
map
{(
$_
->
port
=>
$_
)}
$device
->ports(
undef
, {
prefetch
=> [
'properties'
,
{
neighbor_alias
=>
'device'
}]})->all};
});
register_worker({
phase
=>
'main'
,
driver
=>
'direct'
,
title
=>
'gather macs from file'
},
sub
{
my
(
$job
,
$workerconf
) =
@_
;
my
$device
=
$job
->device;
return
Status->info(
'skip: fwtable data supplied by other source'
)
unless
$job
->is_offline;
my
$data
=
$job
->extra;
if
(
$job
->port) {
return
$job
->cancel(
sprintf
'could not open data source "%s"'
,
$job
->port)
unless
-f
$job
->port;
$data
= read_text(
$job
->port)
or
return
$job
->cancel(
sprintf
'problem reading from file "%s"'
,
$job
->port);
}
my
@fwtable
= (
length
$data
? @{ from_json(
$data
) } : ());
return
$job
->cancel(
'data provided but 0 fwd entries found'
)
unless
scalar
@fwtable
;
debug
sprintf
' [%s] macsuck - %s forwarding table entries provided'
,
$device
->ip,
scalar
@fwtable
;
foreach
my
$node
(
@fwtable
) {
my
$mac
= NetAddr::MAC->new(
mac
=> (
$node
->{
'mac'
} ||
''
));
next
unless
$node
->{
'port'
} and
$mac
;
next
if
((
$mac
->as_ieee eq
'00:00:00:00:00:00'
) or (
$mac
->as_ieee !~ m{^
$RE
{net}{MAC}$}i));
vars->{
'fwtable'
}->{
$node
->{
'vlan'
} || 0 }
->{
$node
->{
'port'
} }
->{
$mac
->as_ieee } += 1;
}
return
Status->done(
"Received MAC addresses for $device"
);
});
register_worker({
phase
=>
'main'
,
driver
=>
'cli'
,
title
=>
'gather macs from CLI'
},
sub
{
my
(
$job
,
$workerconf
) =
@_
;
my
$device
=
$job
->device;
my
$cli
= App::Netdisco::Transport::SSH->session_for(
$device
)
or
return
Status->defer(
"macsuck failed: could not SSH connect to $device"
);
my
$macs
=
$cli
->macsuck;
my
$nodecount
= 0;
foreach
my
$vlan
(
keys
%{
$macs
}) {
foreach
my
$port
(
keys
%{
$macs
->{
$vlan
} }) {
$nodecount
+=
scalar
keys
%{
$macs
->{
$vlan
}->{
$port
} };
}
}
return
$job
->cancel(
'data provided but 0 fwd entries found'
)
unless
$nodecount
;
debug
sprintf
' [%s] macsuck - %s forwarding table entries provided'
,
$device
->ip,
$nodecount
;
vars->{
'fwtable'
} =
$macs
;
return
Status->done(
"Gathered MAC addresses for $device"
);
});
register_worker({
phase
=>
'main'
,
driver
=>
'snmp'
,
title
=>
'gather macs from snmp'
},
sub
{
my
(
$job
,
$workerconf
) =
@_
;
my
$device
=
$job
->device;
my
$snmp
= App::Netdisco::Transport::SNMP->reader_for(
$device
)
or
return
Status->defer(
"macsuck failed: could not SNMP connect to $device"
);
my
$interfaces
=
$snmp
->interfaces || {};
vars->{
'fwtable'
} = walk_fwtable(
$snmp
,
$device
,
$interfaces
);
my
@vlan_list
= get_vlan_list(
$snmp
,
$device
);
{
my
$guard
= guard { snmp_comm_reindex(
$snmp
,
$device
, 0) };
foreach
my
$vlan
(
@vlan_list
) {
snmp_comm_reindex(
$snmp
,
$device
,
$vlan
);
my
$pv_fwtable
=
walk_fwtable(
$snmp
,
$device
,
$interfaces
,
$vlan
);
vars->{
'fwtable'
} = {%{ vars->{
'fwtable'
} },
%$pv_fwtable
};
}
}
return
Status->done(
"Gathered MAC addresses for $device"
);
});
register_worker({
phase
=>
'store'
,
title
=>
'save macs to database'
},
sub
{
my
(
$job
,
$workerconf
) =
@_
;
my
$device
=
$job
->device;
my
@vlans
= (0, sanity_vlans(
$device
, vars->{
'fwtable'
}, {}, {}));
foreach
my
$vlan
(
keys
%{ vars->{
'fwtable'
} }) {
delete
vars->{
'fwtable'
}->{
$vlan
}
unless
scalar
grep
{
$_
eq
$vlan
}
@vlans
;
}
vars->{
'fwtable'
} = sanity_macs(
$device
, vars->{
'fwtable'
}, vars->{
'device_ports'
} );
my
$node_count
= 0;
foreach
my
$vlan
(
reverse
sort
keys
%{ vars->{
'fwtable'
} }) {
foreach
my
$port
(
keys
%{ vars->{
'fwtable'
}->{
$vlan
} }) {
my
$vlabel
= (
$vlan
?
$vlan
:
'unknown'
);
debug
sprintf
' [%s] macsuck - port %s vlan %s : %s nodes'
,
$device
->ip,
$port
,
$vlabel
,
scalar
keys
%{ vars->{
'fwtable'
}->{
$vlan
}->{
$port
} };
foreach
my
$mac
(
keys
%{ vars->{
'fwtable'
}->{
$vlan
}->{
$port
} }) {
delete
vars->{
'fwtable'
}->{0}->{
$_
}->{
$mac
}
for
keys
%{ vars->{
'fwtable'
}->{0} };
store_node(
$device
->ip,
$vlan
,
$port
,
$mac
, vars->{
'timestamp'
});
++
$node_count
;
}
}
}
debug
sprintf
' [%s] macsuck - stored %s forwarding table entries'
,
$device
->ip,
$node_count
;
my
$now
= vars->{
'timestamp'
};
my
$archived
= 0;
if
(setting(
'node_freshness'
)) {
$archived
= schema(
'netdisco'
)->resultset(
'Node'
)->search({
switch
=>
$device
->ip,
time_last
=> \[
"< ($now - ?::interval)"
,
setting(
'node_freshness'
) .
' minutes'
],
})->update({
active
=> \
'false'
});
}
debug
sprintf
' [%s] macsuck - removed %d fwd table entries to archive'
,
$device
->ip,
$archived
;
$device
->update({
last_macsuck
=> \
$now
});
my
$status
=
$job
->best_status;
return
Status->
$status
(
"Ended macsuck for $device"
);
});
sub
store_node {
my
(
$ip
,
$vlan
,
$port
,
$mac
,
$now
) =
@_
;
$now
||=
'LOCALTIMESTAMP'
;
$vlan
||= 0;
my
$oui
= schema(
'netdisco'
)->resultset(
'Manufacturer'
)
->search({
range
=> {
'@>'
=>
\[
q{('x' || lpad( translate( ? ::text, ':', ''), 16, '0')) ::bit(64) ::bigint}
,
$mac
]} },
{
rows
=> 1,
columns
=>
'base'
})->first;
schema(
'netdisco'
)->txn_do(
sub
{
my
$nodes
= schema(
'netdisco'
)->resultset(
'Node'
);
my
$old
=
$nodes
->search(
{
mac
=>
$mac
,
(
$vlan
? (
vlan
=>
$vlan
) : ()),
-bool
=>
'active'
,
-not
=> {
switch
=>
$ip
,
port
=>
$port
,
},
})->update( {
active
=> \
'false'
} );
my
$row
=
$nodes
->update_or_new(
{
switch
=>
$ip
,
port
=>
$port
,
vlan
=>
$vlan
,
mac
=>
$mac
,
active
=> \
'true'
,
oui
=> (
$oui
?
$oui
->base :
undef
),
time_last
=> \
$now
,
((
$old
!= 0) ? (
time_recent
=> \
$now
) : ()),
},
{
key
=>
'primary'
,
for
=>
'update'
,
}
);
if
(!
$row
->in_storage) {
$row
->set_column(
time_first
=> \
$now
);
$row
->insert;
}
});
}
sub
get_vlan_list {
my
(
$snmp
,
$device
) =
@_
;
return
()
unless
$snmp
->cisco_comm_indexing;
my
(
%vlans
,
%vlan_names
,
%vlan_states
);
my
$i_vlan
=
$snmp
->i_vlan || {};
my
$trunks
=
$snmp
->i_vlan_membership || {};
my
$i_type
=
$snmp
->i_type || {};
while
(
my
(
$idx
,
$vlan
) =
each
%$i_vlan
) {
$vlan
=~ s/^\d+\.//;
if
(
exists
$i_type
->{
$idx
} and
$i_type
->{
$idx
} eq
'propVirtual'
) {
$vlans
{
$vlan
} ||= 0;
}
else
{
++
$vlans
{
$vlan
};
}
foreach
my
$t_vlan
(@{
$trunks
->{
$idx
}}) {
++
$vlans
{
$t_vlan
};
}
}
unless
(
scalar
keys
%vlans
) {
debug
sprintf
' [%s] macsuck - no VLANs found.'
,
$device
->ip;
return
();
}
my
$v_name
=
$snmp
->v_name || {};
while
(
my
(
$idx
,
$name
) =
each
%$v_name
) {
(
my
$vlan
=
$idx
) =~ s/^\d+\.//;
$vlans
{
$vlan
} ||= 0;
$vlan_names
{
$vlan
} =
$name
;
}
debug
sprintf
' [%s] macsuck - VLANs: %s'
,
$device
->ip,
(
join
','
,
sort
grep
{
$_
}
keys
%vlans
);
my
$v_state
=
$snmp
->v_state || {};
while
(
my
(
$idx
,
$state
) =
each
%$v_state
) {
(
my
$vlan
=
$idx
) =~ s/^\d+\.//;
$vlans
{
$vlan
} ||= 0;
$vlan_states
{
$vlan
} =
$state
;
}
return
sanity_vlans(
$device
, \
%vlans
, \
%vlan_names
, \
%vlan_states
);
}
sub
sanity_vlans {
my
(
$device
,
$vlans
,
$vlan_names
,
$vlan_states
) =
@_
;
my
@ok_vlans
= ();
foreach
my
$vlan
(
sort
keys
%$vlans
) {
my
$name
=
$vlan_names
->{
$vlan
} ||
'(unnamed)'
;
my
$state
=
$vlan_states
->{
$vlan
} ||
'(unknown)'
;
if
(
ref
[] eq
ref
setting(
'macsuck_no_vlan'
)) {
my
$ignore
= setting(
'macsuck_no_vlan'
);
if
((
scalar
grep
{
$_
eq
$vlan
}
@$ignore
) or
(
scalar
grep
{
$_
eq
$name
}
@$ignore
)) {
debug
sprintf
' [%s] macsuck VLAN %s - skipped by macsuck_no_vlan config'
,
$device
->ip,
$vlan
;
next
;
}
}
if
(
ref
[] eq
ref
setting(
'macsuck_no_devicevlan'
)) {
my
$ignore
= setting(
'macsuck_no_devicevlan'
);
my
$ip
=
$device
->ip;
if
((
scalar
grep
{
$_
eq
"$ip:$vlan"
}
@$ignore
) or
(
scalar
grep
{
$_
eq
"$ip:$name"
}
@$ignore
)) {
debug
sprintf
' [%s] macsuck VLAN %s - skipped by macsuck_no_devicevlan config'
,
$device
->ip,
$vlan
;
next
;
}
}
if
(setting(
'macsuck_no_unnamed'
) and
$name
eq
'(unnamed)'
) {
debug
sprintf
' [%s] macsuck VLAN %s - skipped by macsuck_no_unnamed config'
,
$device
->ip,
$vlan
;
next
;
}
if
(
$vlan
> 4094) {
debug
sprintf
' [%s] macsuck - invalid VLAN number %s'
,
$device
->ip,
$vlan
;
next
;
}
next
if
$vlan
== 0;
if
(not
$vlans
->{
$vlan
} and not setting(
'macsuck_all_vlans'
)) {
debug
sprintf
' [%s] macsuck VLAN %s/%s - not in use by any port - skipping.'
,
$device
->ip,
$vlan
,
$name
;
next
;
}
if
(
$state
eq
'suspended'
) {
debug
sprintf
' [%s] macsuck VLAN %s - VLAN is suspended - skipping.'
,
$device
->ip,
$vlan
;
next
;
}
push
@ok_vlans
,
$vlan
;
}
return
@ok_vlans
;
}
sub
walk_fwtable {
my
(
$snmp
,
$device
,
$interfaces
,
$comm_vlan
) =
@_
;
my
$cache
= {};
my
$fw_mac
=
$snmp
->fw_mac || {};
my
$fw_port
=
$snmp
->fw_port || {};
my
$fw_vlan
= (
$snmp
->can(
'cisco_comm_indexing'
) and
$snmp
->cisco_comm_indexing())
? {} :
$snmp
->qb_fw_vlan;
my
$bp_index
=
$snmp
->bp_index || {};
MAC:
while
(
my
(
$idx
,
$mac
) =
each
%$fw_mac
) {
my
$bp_id
=
$fw_port
->{
$idx
};
unless
(
defined
$bp_id
) {
debug
sprintf
' [%s] macsuck %s - %s has no fw_port mapping - skipping.'
,
$device
->ip,
$mac
,
$idx
;
next
MAC;
}
my
$iid
=
$bp_index
->{
$bp_id
};
my
$vlan
=
$fw_vlan
->{
$idx
} ||
$comm_vlan
||
'0'
;
unless
(
defined
$iid
) {
debug
sprintf
' [%s] macsuck %s - port %s has no bp_index mapping - skipping.'
,
$device
->ip,
$mac
,
$bp_id
;
next
MAC;
}
my
$port
=
$interfaces
->{
$iid
};
unless
(
defined
$port
) {
debug
sprintf
' [%s] macsuck %s - iid %s has no port mapping - skipping.'
,
$device
->ip,
$mac
,
$iid
;
next
MAC;
}
++
$cache
->{
$vlan
}->{
$port
}->{
$mac
};
}
return
$cache
;
}
sub
sanity_macs {
my
(
$device
,
$cache
,
$device_ports
) =
@_
;
my
@fw_mac_list
= ();
foreach
my
$vlan
(
keys
%{
$cache
}) {
foreach
my
$port
(
keys
%{
$cache
->{
$vlan
} }) {
push
@fw_mac_list
,
keys
%{
$cache
->{
$vlan
}->{
$port
} };
}
}
@fw_mac_list
= List::MoreUtils::uniq(
@fw_mac_list
);
my
$port_macs
= get_port_macs(\
@fw_mac_list
);
my
$neighborport
= {};
my
$ignoreport
= {};
if
(
scalar
@{ setting(
'macsuck_no_deviceports'
) }) {
my
@ignoremaps
= @{ setting(
'macsuck_no_deviceports'
) };
foreach
my
$map
(
@ignoremaps
) {
next
unless
ref
{} eq
ref
$map
;
foreach
my
$key
(
sort
keys
%$map
) {
next
unless
$key
and
$map
->{
$key
};
next
unless
acl_matches(
$device
,
$key
);
foreach
my
$port
(
sort
{ sort_port(
$a
,
$b
) }
keys
%{
$device_ports
}) {
next
unless
acl_matches(
$device_ports
->{
$port
},
$map
->{
$key
});
debug
sprintf
' [%s] macsuck %s - port suppressed by macsuck_no_deviceports'
,
$device
->ip,
$port
;
++
$ignoreport
->{
$port
};
}
}
}
}
foreach
my
$vlan
(
keys
%{
$cache
}) {
foreach
my
$port
(
keys
%{
$cache
->{
$vlan
} }) {
MAC:
foreach
my
$mac
(
keys
%{
$cache
->{
$vlan
}->{
$port
} }) {
unless
(check_mac(
$mac
,
$device
)) {
delete
$cache
->{
$vlan
}->{
$port
}->{
$mac
};
next
MAC;
}
my
$device_port
=
$device_ports
->{
$port
};
unless
(
defined
$device_port
) {
debug
sprintf
' [%s] macsuck %s - port %s is not in database - skipping.'
,
$device
->ip,
$mac
,
$port
;
delete
$cache
->{
$vlan
}->{
$port
}->{
$mac
};
next
MAC;
}
if
(
exists
$ignoreport
->{
$port
}) {
debug
sprintf
' [%s] macsuck %s - port %s is suppressed by config - skipping.'
,
$device
->ip,
$mac
,
$port
;
delete
$cache
->{
$vlan
}->{
$port
}->{
$mac
};
next
MAC;
}
if
(
exists
$neighborport
->{
$port
}) {
debug
sprintf
' [%s] macsuck %s - seen another device thru port %s - skipping.'
,
$device
->ip,
$mac
,
$port
;
delete
$cache
->{
$vlan
}->{
$port
}->{
$mac
};
next
MAC;
}
my
$neigh_cannot_macsuck
=
eval
{
acl_matches((
$device_port
->neighbor ||
"0 but true"
),
'macsuck_unsupported'
) ||
match_to_setting(
$device_port
->remote_type,
'macsuck_unsupported_type'
) };
if
(
$device_port
->is_uplink) {
if
(
$neigh_cannot_macsuck
) {
debug
sprintf
' [%s] macsuck %s - port %s neighbor %s without macsuck support'
,
$device
->ip,
$mac
,
$port
,
(
eval
{
$device_port
->neighbor->ip }
|| (
$device_port
->remote_ip
||
$device_port
->remote_id ||
'?'
));
}
elsif
(
my
$neighbor
=
$device_port
->neighbor) {
debug
sprintf
' [%s] macsuck %s - port %s has neighbor %s - skipping.'
,
$device
->ip,
$mac
,
$port
,
$neighbor
->ip;
delete
$cache
->{
$vlan
}->{
$port
}->{
$mac
};
next
MAC;
}
elsif
(
my
$remote
=
$device_port
->remote_ip) {
debug
sprintf
' [%s] macsuck %s - port %s has undiscovered neighbor %s'
,
$device
->ip,
$mac
,
$port
,
$remote
;
}
elsif
(not setting(
'macsuck_bleed'
)) {
debug
sprintf
' [%s] macsuck %s - port %s is detected uplink - skipping.'
,
$device
->ip,
$mac
,
$port
;
$neighborport
->{
$port
} = [
$vlan
,
$mac
]
if
exists
$port_macs
->{
$mac
};
delete
$cache
->{
$vlan
}->{
$port
}->{
$mac
};
next
MAC;
}
}
if
(
exists
$port_macs
->{
$mac
}) {
my
$switch_ip
=
$port_macs
->{
$mac
};
if
(
$device
->ip eq
$switch_ip
) {
debug
sprintf
' [%s] macsuck %s - port %s connects to self - skipping.'
,
$device
->ip,
$mac
,
$port
;
delete
$cache
->{
$vlan
}->{
$port
}->{
$mac
};
next
MAC;
}
debug
sprintf
' [%s] macsuck %s - port %s is probably an uplink'
,
$device
->ip,
$mac
,
$port
;
$device_port
->update({
is_uplink
=> \
'true'
});
if
(
$neigh_cannot_macsuck
) {
delete
$cache
->{
$vlan
}->{
$port
}->{
$mac
};
next
MAC;
}
if
(not setting(
'macsuck_bleed'
)) {
debug
sprintf
' [%s] macsuck %s - port %s is at topology edge'
,
$device
->ip,
$mac
,
$port
;
$neighborport
->{
$port
} = [
$vlan
,
$mac
];
delete
$cache
->{
$vlan
}->{
$port
}->{
$mac
};
next
MAC;
}
}
if
(
defined
$device_port
->slave_of
and
exists
$device_ports
->{
$device_port
->slave_of}) {
my
$parent
=
$device_port
->slave_of;
$device_ports
->{
$parent
}->update({
is_uplink
=> \
'true'
});
if
(
$device_ports
->{
$parent
}->is_master) {
delete
$cache
->{
$vlan
}->{
$port
}->{
$mac
};
++
$cache
->{
$vlan
}->{
$parent
}->{
$mac
};
}
}
}
}
}
foreach
my
$port
(
keys
%$neighborport
) {
my
(
$vlan
,
$mac
) = @{
$neighborport
->{
$port
} };
delete
$cache
->{
$_
}->{
$port
}
for
keys
%$cache
;
++
$cache
->{
$vlan
}->{
$port
}->{
$mac
};
}
return
$cache
;
}
true;