X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=arper.py;h=4d6a3a2baa8900a4e6f5f535ad846766b2d353d7;hb=ed8fa2b10b0177b15b7423263bdd390efde2f0c8;hp=73d1dd654bf1961bbe6bdc52ec8fd99d3d7db064;hpb=29ee1f98654a689e9cab76b0c7c68428faa43a8c;p=python_utils.git diff --git a/arper.py b/arper.py index 73d1dd6..4d6a3a2 100644 --- a/arper.py +++ b/arper.py @@ -1,16 +1,22 @@ #!/usr/bin/env python3 +"""A caching layer around the kernel's network mapping between IPs and MACs""" + import datetime import logging import os from typing import Any, Optional +from overrides import overrides + import argparse_utils from collect.bidict import BiDict import config import exec_utils +import file_utils import persistent import string_utils +import site_config logger = logging.getLogger(__name__) @@ -25,31 +31,42 @@ cfg.add_argument( help='Where to cache the kernel ARP table', ) cfg.add_argument( - '--arp_cache_max_staleness', + '--arper_cache_max_staleness', type=argparse_utils.valid_duration, - default=datetime.timedelta(seconds=60 * 5), + default=datetime.timedelta(seconds=60 * 60), metavar='DURATION', help='Max acceptable age of the kernel arp table cache' ) +cfg.add_argument( + '--arper_min_entries_to_be_valid', + type=int, + default=site_config.get_config().arper_minimum_device_count, + help='Min number of arp entries to bother persisting.' +) @persistent.persistent_autoloaded_singleton() class Arper(persistent.Persistent): - def __init__(self, cached_state: Optional[BiDict[str, str]] = None) -> None: + def __init__( + self, cached_state: Optional[BiDict[str, str]] = None + ) -> None: self.state = BiDict() if cached_state is not None: logger.debug('Loading Arper map from cached state.') self.state = cached_state else: logger.debug('No usable cached state; calling /usr/sbin/arp') - output = exec_utils.cmd( - '/usr/sbin/arp -a', - timeout_seconds=5.0 - ) - for line in output.split('\n'): - line = str(line, 'ascii') - ip = string_utils.extract_ip_v4(line) - mac = string_utils.extract_mac_address(line) + self.update() + + def update(self): + output = exec_utils.cmd( + '/usr/sbin/arp -a', + timeout_seconds=5.0 + ) + for line in output.split('\n'): + ip = string_utils.extract_ip_v4(line) + mac = string_utils.extract_mac_address(line) + if ip is not None and mac is not None: mac = mac.lower() logger.debug(f' {mac} => {ip}') self.state[mac] = ip @@ -61,28 +78,50 @@ class Arper(persistent.Persistent): def get_mac_by_ip(self, ip: str) -> Optional[str]: return self.state.inverse.get(ip, None) - def save(self) -> bool: - logger.debug(f'Persisting state to {config.config["arp_cache_location"]}') - with open(config.config['arp_cache_location'], 'w') as wf: - for (mac, ip) in self.state.items(): - mac = mac.lower() - print(f'{mac}, {ip}', file=wf) - @classmethod + @overrides def load(cls) -> Any: + cache_file = config.config['arper_cache_location'] if persistent.was_file_written_within_n_seconds( - config.config['arp_cache_location'], - config.config['arp_cache_max_staleness'].total_seconds(), + cache_file, + config.config['arper_cache_max_staleness'].total_seconds(), ): - logger.debug(f'Loading state from {config.config["arp_cache_location"]}') + logger.debug(f'Loading state from {cache_file}') cached_state = BiDict() - with open(config.config['arp_cache_location'], 'r') as rf: - for line in rd.readline(): + with open(cache_file, 'r') as rf: + contents = rf.readlines() + for line in contents: + line = line[:-1] + logger.debug(f'ARPER:{cache_file}> {line}') (mac, ip) = line.split(',') mac = mac.strip() mac = mac.lower() ip = ip.strip() cached_state[mac] = ip - return Arper(cached_state) + if len(cached_state) > config.config['arper_min_entries_to_be_valid']: + return cls(cached_state) + else: + logger.warning( + f'{cache_file} sucks, only {len(cached_state)} entries. Deleting it.' + ) + os.remove(cache_file) + logger.debug('No usable saved state found') return None + + @overrides + def save(self) -> bool: + if len(self.state) > config.config['arper_min_entries_to_be_valid']: + logger.debug( + f'Persisting state to {config.config["arper_cache_location"]}' + ) + with file_utils.FileWriter(config.config['arper_cache_location']) as wf: + for (mac, ip) in self.state.items(): + mac = mac.lower() + print(f'{mac}, {ip}', file=wf) + return True + else: + logger.warning( + f'Only saw {len(self.state)} entries; needed at least {config.config["arper_min_entries_to_be_valid"]} to bother persisting.' + ) + return False