mirror of
				https://github.com/librenms/librenms.git
				synced 2024-10-07 16:52:45 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			253 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			253 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
#!/usr/bin/env python
 | 
						|
"""
 | 
						|
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>
 | 
						|
"""
 | 
						|
 | 
						|
from __future__ import print_function
 | 
						|
from __future__ import unicode_literals
 | 
						|
 | 
						|
import argparse
 | 
						|
from argparse import RawTextHelpFormatter
 | 
						|
import json
 | 
						|
from collections import namedtuple
 | 
						|
from multiprocessing import Pool
 | 
						|
from os import path, chdir
 | 
						|
from socket import gethostbyname, gethostbyaddr, herror, gaierror
 | 
						|
from subprocess import check_output, CalledProcessError
 | 
						|
from sys import stdout
 | 
						|
from time import time
 | 
						|
 | 
						|
try:
 | 
						|
    from ipaddress import ip_network, ip_address
 | 
						|
except:
 | 
						|
    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')
 | 
						|
    exit(2)
 | 
						|
 | 
						|
Result = namedtuple('Result', ['ip', 'hostname', 'outcome', 'output'])
 | 
						|
 | 
						|
 | 
						|
class Outcome:
 | 
						|
    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()
 | 
						|
stats = {
 | 
						|
    'count': 0,
 | 
						|
    Outcome.ADDED: 0,
 | 
						|
    Outcome.UNPINGABLE: 0,
 | 
						|
    Outcome.KNOWN: 0,
 | 
						|
    Outcome.FAILED: 0,
 | 
						|
    Outcome.EXCLUDED: 0,
 | 
						|
    Outcome.TERMINATED: 0
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
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:
 | 
						|
        print('Scanned \033[1m{}\033[0m {}'.format(
 | 
						|
            ("{} ({})".format(data.hostname, data.ip) if data.hostname else data.ip), data.output))
 | 
						|
    else:
 | 
						|
        print(get_outcome_symbol(data.outcome), end='')
 | 
						|
        stdout.flush()
 | 
						|
 | 
						|
    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:
 | 
						|
            # attempt to convert IP to hostname, if anything goes wrong, just use the IP
 | 
						|
            tmp = gethostbyaddr(ip)[0]
 | 
						|
            if gethostbyname(tmp) == ip:  # check that forward resolves
 | 
						|
                hostname = tmp
 | 
						|
        except (herror, gaierror):
 | 
						|
            pass
 | 
						|
 | 
						|
        try:
 | 
						|
            arguments = ['/usr/bin/env', 'php', 'addhost.php', hostname or ip]
 | 
						|
            if args.ping:
 | 
						|
                arguments.insert(3, args.ping)
 | 
						|
            add_output = check_output(arguments)
 | 
						|
            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 #
 | 
						|
    ###################
 | 
						|
    parser = argparse.ArgumentParser(description='Scan network for snmp hosts and add them to LibreNMS.', formatter_class=RawTextHelpFormatter)
 | 
						|
    parser.add_argument('network', action='append', nargs='*', type=str, help="""CIDR noted IP-Range to scan. Can be specified multiple times
 | 
						|
This argument is only required if 'nets' config is not set
 | 
						|
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""")
 | 
						|
    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))
 | 
						|
    parser.add_argument('-l', '--legend', action='store_true', help="Print the legend.")
 | 
						|
    parser.add_argument('-v', '--verbose', action='count',
 | 
						|
                        help="Show debug output. Specifying multiple times increases the verbosity.")
 | 
						|
 | 
						|
    # compatibility arguments
 | 
						|
    parser.add_argument('-r', dest='network', action='append', help=argparse.SUPPRESS)
 | 
						|
    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()))
 | 
						|
        exit(2)
 | 
						|
 | 
						|
    #######################
 | 
						|
    # Build network lists #
 | 
						|
    #######################
 | 
						|
 | 
						|
    # 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:
 | 
						|
        parser.error('\'nets\' is not set in your LibreNMS config, you must specify a network to scan')
 | 
						|
 | 
						|
    # check for valid networks
 | 
						|
    networks = []
 | 
						|
    for net in (netargs if netargs else CONFIG.get('nets', [])):
 | 
						|
        try:
 | 
						|
            networks.append(ip_network(u'%s' % net, True))
 | 
						|
            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))
 |