2017-07-12 02:00:01 -05:00
#!/usr/bin/env python
2017-07-03 15:57:56 -05:00
"""
Scan networks for snmp hosts and add them to LibreNMS
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
@package LibreNMS
@link http://librenms.org
@copyright 2017 Tony Murray
@author Tony Murray <murraytony@gmail.com>
"""
2017-07-12 02:00:01 -05:00
from __future__ import print_function
from __future__ import unicode_literals
2017-07-03 15:57:56 -05:00
import argparse
2017-10-28 05:59:25 +02:00
from argparse import RawTextHelpFormatter
2017-07-03 15:57:56 -05:00
import json
from collections import namedtuple
from multiprocessing import Pool
from os import path , chdir
2018-05-04 09:46:18 -05:00
from socket import gethostbyname , gethostbyaddr , herror , gaierror
2017-07-03 15:57:56 -05:00
from subprocess import check_output , CalledProcessError
2017-07-12 02:00:01 -05:00
from sys import stdout
2017-07-03 15:57:56 -05:00
from time import time
2017-07-12 02:00:01 -05:00
try :
from ipaddress import ip_network , ip_address
except :
2017-07-17 13:00:58 -05:00
print ( 'Could not import ipaddress module. Please install python-ipaddress or use python3 to run this script' )
print ( 'Debian/Ubuntu: apt install python-ipaddress' )
print ( 'RHEL/CentOS: yum install python-ipaddress' )
2017-07-12 02:00:01 -05:00
exit ( 2 )
2017-07-03 15:57:56 -05:00
Result = namedtuple ( 'Result' , [ 'ip' , 'hostname' , 'outcome' , 'output' ])
2017-07-14 15:56:47 -05:00
class Outcome :
2017-07-03 15:57:56 -05:00
UNDEFINED = 0
ADDED = 1
UNPINGABLE = 2
KNOWN = 3
FAILED = 4
EXCLUDED = 5
TERMINATED = 6
VERBOSE_LEVEL = 0
THREADS = 32
CONFIG = {}
EXCLUDED_NETS = []
start_time = time ()
2017-07-12 02:00:01 -05:00
stats = {
'count' : 0 ,
Outcome . ADDED : 0 ,
Outcome . UNPINGABLE : 0 ,
Outcome . KNOWN : 0 ,
Outcome . FAILED : 0 ,
Outcome . EXCLUDED : 0 ,
Outcome . TERMINATED : 0
}
2017-07-03 15:57:56 -05:00
def debug ( message , level = 2 ):
if level <= VERBOSE_LEVEL :
print ( message )
def get_outcome_symbol ( outcome ):
return {
Outcome . UNDEFINED : '?' , # should not occur
Outcome . ADDED : '+' ,
Outcome . UNPINGABLE : '.' ,
Outcome . KNOWN : '*' ,
Outcome . FAILED : '-' ,
Outcome . TERMINATED : ''
}[ outcome ]
def handle_result ( data ):
if VERBOSE_LEVEL > 0 :
2017-07-12 02:00:01 -05:00
print ( 'Scanned \033 [1m {} \033 [0m {} ' . format (
( " {} ( {} )" . format ( data . hostname , data . ip ) if data . hostname else data . ip ), data . output ))
2017-07-03 15:57:56 -05:00
else :
2017-07-12 02:00:01 -05:00
print ( get_outcome_symbol ( data . outcome ), end = '' )
stdout . flush ()
2017-07-03 15:57:56 -05:00
stats [ 'count' ] += 0 if data . outcome == Outcome . TERMINATED else 1
stats [ data . outcome ] += 1
def check_ip_excluded ( ip ):
for net in EXCLUDED_NETS :
if ip in net :
debug ( " \033 [91m {} excluded by autodiscovery.nets-exclude \033 [0m" . format ( ip ), 1 )
stats [ Outcome . EXCLUDED ] += 1
return True
return False
def scan_host ( ip ):
hostname = None
try :
try :
2018-05-04 09:46:18 -05:00
# attempt to convert IP to hostname, if anything goes wrong, just use the IP
2017-07-12 02:00:01 -05:00
tmp = gethostbyaddr ( ip )[ 0 ]
if gethostbyname ( tmp ) == ip : # check that forward resolves
2017-07-03 15:57:56 -05:00
hostname = tmp
2018-05-04 09:46:18 -05:00
except ( herror , gaierror ):
2017-07-03 15:57:56 -05:00
pass
try :
2017-11-01 18:49:38 +01:00
arguments = [ '/usr/bin/env' , 'php' , 'addhost.php' , hostname or ip ]
if args . ping :
arguments . insert ( 3 , args . ping )
add_output = check_output ( arguments )
2017-07-03 15:57:56 -05:00
return Result ( ip , hostname , Outcome . ADDED , add_output )
except CalledProcessError as err :
output = err . output . decode () . rstrip ()
if err . returncode == 2 :
if 'Could not ping' in output :
return Result ( ip , hostname , Outcome . UNPINGABLE , output )
else :
return Result ( ip , hostname , Outcome . FAILED , output )
elif err . returncode == 3 :
return Result ( ip , hostname , Outcome . KNOWN , output )
except KeyboardInterrupt :
return Result ( ip , hostname , Outcome . TERMINATED , 'Terminated' )
return Result ( ip , hostname , Outcome . UNDEFINED , output )
if __name__ == '__main__' :
###################
# Parse arguments #
###################
2017-10-28 05:59:25 +02:00
parser = argparse . ArgumentParser ( description = 'Scan network for snmp hosts and add them to LibreNMS.' , formatter_class = RawTextHelpFormatter )
2017-07-12 02:00:01 -05:00
parser . add_argument ( 'network' , action = 'append' , nargs = '*' , type = str , help = """CIDR noted IP-Range to scan. Can be specified multiple times
2019-06-23 00:29:12 -05:00
This argument is only required if 'nets' config is not set
2017-10-28 05:59:25 +02:00
Example: 192.168.0.0/24
Example: 192.168.0.0/31 will be treated as an RFC3021 p-t-p network with two addresses, 192.168.0.0 and 192.168.0.1
Example: 192.168.0.1/32 will be treated as a single host address""" )
parser . add_argument ( '-P' , '--ping' , action = 'store_const' , const = "-b" , default = "" , help = """Add the device as an ICMP only device if it replies to ping but not SNMP.
Example: """ + __file__ + """ -P 192.168.0.0/24""" )
2017-07-12 02:00:01 -05:00
parser . add_argument ( '-t' , dest = 'threads' , type = int ,
help = "How many IPs to scan at a time. More will increase the scan speed," +
" but could overload your system. Default: {} " . format ( THREADS ))
2017-07-03 15:57:56 -05:00
parser . add_argument ( '-l' , '--legend' , action = 'store_true' , help = "Print the legend." )
2017-07-12 02:00:01 -05:00
parser . add_argument ( '-v' , '--verbose' , action = 'count' ,
help = "Show debug output. Specifying multiple times increases the verbosity." )
2017-07-03 15:57:56 -05:00
# compatibility arguments
2017-07-12 02:00:01 -05:00
parser . add_argument ( '-r' , dest = 'network' , action = 'append' , help = argparse . SUPPRESS )
2017-07-03 15:57:56 -05:00
parser . add_argument ( '-d' , '-i' , dest = 'verbose' , action = 'count' , help = argparse . SUPPRESS )
parser . add_argument ( '-n' , action = 'store_true' , help = argparse . SUPPRESS )
parser . add_argument ( '-b' , action = 'store_true' , help = argparse . SUPPRESS )
args = parser . parse_args ()
VERBOSE_LEVEL = args . verbose or VERBOSE_LEVEL
THREADS = args . threads or THREADS
# Import LibreNMS config
install_dir = path . dirname ( path . realpath ( __file__ ))
chdir ( install_dir )
try :
CONFIG = json . loads ( check_output ([ '/usr/bin/env' , 'php' , 'config_to_json.php' ]) . decode ())
except CalledProcessError as e :
parser . error ( "Could not execute: {} \n {} " . format ( ' ' . join ( e . cmd ), e . output . decode () . rstrip ()))
2017-07-12 02:00:01 -05:00
exit ( 2 )
2017-07-03 15:57:56 -05:00
#######################
# Build network lists #
#######################
2017-07-12 02:00:01 -05:00
# fix argparse awkwardness
netargs = []
for a in args . network :
if type ( a ) is list :
netargs += a
else :
netargs . append ( a )
# make sure we have something to scan
if not CONFIG . get ( 'nets' , []) and not netargs :
2019-06-23 00:29:12 -05:00
parser . error ( ' \' nets \' is not set in your LibreNMS config, you must specify a network to scan' )
2017-07-12 02:00:01 -05:00
# check for valid networks
2017-07-03 15:57:56 -05:00
networks = []
2017-07-12 02:00:01 -05:00
for net in ( netargs if netargs else CONFIG . get ( 'nets' , [])):
2017-07-03 15:57:56 -05:00
try :
2017-07-12 02:00:01 -05:00
networks . append ( ip_network ( u ' %s ' % net , True ))
2017-07-03 15:57:56 -05:00
debug ( 'Network parsed: {} ' . format ( net ), 2 )
except ValueError as e :
parser . error ( 'Invalid network format {} ' . format ( e ))
for net in CONFIG . get ( 'autodiscovery' , {}) . get ( 'nets-exclude' , {}):
try :
EXCLUDED_NETS . append ( ip_network ( net , True ))
debug ( 'Excluded network: {} ' . format ( net ), 2 )
except ValueError as e :
parser . error ( 'Invalid excluded network format {} , check your config.php' . format ( e ))
#################
# Scan networks #
#################
debug ( 'SNMP settings from config.php: {} ' . format ( CONFIG . get ( 'snmp' , {})), 2 )
if args . legend and not VERBOSE_LEVEL :
print ( 'Legend: \n + Added device \n * Known device \n - Failed to add device \n . Ping failed \n ' )
print ( 'Scanning IPs:' )
pool = Pool ( processes = THREADS )
try :
for network in networks :
if network . num_addresses == 1 :
ips = [ ip_address ( network . network_address )]
else :
ips = network . hosts ()
for ip in ips :
if not check_ip_excluded ( ip ):
pool . apply_async ( scan_host , ( str ( ip ),), callback = handle_result )
pool . close ()
pool . join ()
except KeyboardInterrupt :
pool . terminate ()
if VERBOSE_LEVEL == 0 :
print ( " \n " )
base = 'Scanned {} IPs: {} known devices, added {} devices, failed to add {} devices'
summary = base . format ( stats [ 'count' ], stats [ Outcome . KNOWN ], stats [ Outcome . ADDED ], stats [ Outcome . FAILED ])
if stats [ Outcome . EXCLUDED ]:
summary += ', {} ips excluded by config' . format ( stats [ Outcome . EXCLUDED ])
print ( summary )
print ( 'Runtime: {:.2f} seconds' . format ( time () - start_time ))