jaesivsm/MindYourNeighbors

View on GitHub
src/mind_your_neighbors/main.py

Summary

Maintainability
B
6 hrs
Test Coverage
import re
import logging
from datetime import datetime
from collections import defaultdict

from cronex import CronExpression

from mind_your_neighbors import utils, const, commands, cache

logger = logging.getLogger('MindYourNeighbors')


def _split(string, lower=True):
    return [split.strip().lower() if lower else split.strip()
            for split in string.split(',')]


def _to_filter_on_mac(list_, mapping):
    return {mapping[str_] for str_ in _split(list_) if str_ in mapping}


def logging_results(addr_by_mac, known_machines):
    """Will fire several logging message with levels depending on matching
    status"""
    rev_machine = {mac: name for name, mac in known_machines.items()} \
            if known_machines else {}

    for loglevel, key in const.LOG_TO_MATCH_RES_MAPPING:
        if logger.isEnabledFor(loglevel):
            for mac, addrs in addr_by_mac[key].items():
                message = '%s - %s' % (key.name, mac)
                written = False
                if mac in rev_machine:
                    written = True
                    message += ' - MACHINE: %s' % rev_machine[mac]
                if not written and addrs:
                    message += ' - ADDRS: ' + ' '.join(addrs)
                logger.log(loglevel, message)


def process_filters(filter_on_regex, filter_out_regex, exclude,
                    filter_on_machines, filter_out_machines, known_machines):
    filter_on, filter_out = [const.REACHABLE.match], []
    filter_on_mac, filter_out_mac = set(), set()

    if filter_on_regex:
        filter_on.append(re.compile(filter_on_regex).match)
    if filter_out_regex:
        filter_out.append(re.compile(filter_out_regex).match)
    if known_machines and filter_on_machines:
        filter_on_mac = _to_filter_on_mac(filter_on_machines, known_machines)
    if known_machines and filter_out_machines:
        filter_out_mac = _to_filter_on_mac(filter_out_machines, known_machines)
    if exclude:
        filter_out.append(lambda string: any(value in string
                            for value in _split(exclude, lower=False)))

    return filter_on, filter_out, filter_on_mac, filter_out_mac


def check_neighborhood(neighbors, filter_on_regex=None, filter_out_regex=None,
                       filter_on_machines=None, filter_out_machines=None,
                       exclude=None, known_machines=None):
    """Will execute *ip neigh* unless the result of the command has been
    cached. Will then compile a specific regex for the given parameters and
    return True if matching result means there is someone in the local network.
    """

    filter_on, filter_out, filter_on_mac, filter_out_mac = process_filters(
            filter_on_regex, filter_out_regex, exclude, filter_on_machines,
            filter_out_machines, known_machines)
    result = defaultdict(list)
    addr_by_mac = defaultdict(lambda: defaultdict(list))
    for line, addr, mac in neighbors:
        if any(match(line) for match in filter_out) or mac in filter_out_mac:
            key = const.MatchResult.EXCLUDED
        elif all(match(line) for match in filter_on) \
                and (not filter_on_mac or mac in filter_on_mac):
            key = const.MatchResult.MATCHED
        else:
            key = const.MatchResult.NO_MATCH
        result[key].append(line)
        addr_by_mac[key][mac].append(addr)

    logging_results(addr_by_mac, known_machines)
    return bool(result[const.MatchResult.MATCHED])


def handle_processes(processes, config, cache):
    """Will check on processes launched during config browsing and log result
    """
    for section, process in processes.items():
        if not config.getboolean(section, 'error_on_stderr', fallback=False):
            continue
        stdout, stderr = process.communicate()
        logger.debug(stdout)
        if not stderr:
            continue
        logger.error('%r - an error occured, removing stored command',
                     section.name)
        cache.section_name = section.name
        cache.cache_command(None)
        logger.error('%r - command stderr was: %r', section.name, stderr)


@cache.wrap
def browse_config(config, cache):
    """Will browse all section of the config,
    fill cache and launch command when needed.
    """
    commands.ip_neigh.cache_clear()
    processes = {}
    now = datetime.now()
    now = (now.year, now.month, now.day, now.hour, now.minute)
    excluded_sections = {config.default_section, const.KNOWN_MACHINES_SECTION}
    known_machines = utils.get_known_machines(config)
    for section in config.values():
        if section.name in excluded_sections:
            continue

        if not section.getboolean('enabled'):
            logger.debug('section %r not enabled', section)
            continue

        cron = section.get('cron')
        if cron and not CronExpression(cron).check_trigger(now):
            logger.debug('section %r disabled for now', section)
            continue

        logger.debug('%r - processing section', section.name)
        cache.section_name = section.name
        device = section.get('device')
        neighbors = commands.ip_neigh(device=device)

        threshold = section.getint('threshold')

        if check_neighborhood(neighbors,
                              section.get('filter_on_regex'),
                              section.get('filter_out_regex'),
                              section.get('filter_on_machines'),
                              section.get('filter_out_machines'),
                              section.get('exclude'),
                              known_machines=known_machines):
            cmd = section.get('command_neighbor')
            result = 'neighbor'
        else:
            cmd = section.get('command_no_neighbor')
            result = 'no_neighbor'

        cache.cache_result(result, threshold)
        logger.info('%r - cache state: %r', section.name, cache.section)
        count = cache.get_result_count(result)
        if count != threshold:
            logger.info("%r - cache count hasn't reached threshold yet "
                        "(%d/%d)", section.name, count, threshold)
            continue
        if cache.last_command == cmd:
            logger.info('%r - command has already been run', section.name)
            continue

        cache.cache_command(cmd)
        if cmd:
            logger.warning('%r - launching: %r', section.name, cmd)
            processes[section.name] = commands.execute(cmd.split())
        else:
            logger.info('%r - no command to launch', section.name)

    handle_processes(processes, config, cache)