From 13e2ad175f01b6cfbdb21ee50e85b6f3ba6fe750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sun, 25 Apr 2021 12:45:34 +0200 Subject: [PATCH] [scanner] add host class, name mapper and pre-config logic --- cdist/argparse.py | 6 +- cdist/scan/commandline.py | 37 ++++++++---- cdist/scan/scan.py | 122 +++++++++++++++++++++++++++++--------- 3 files changed, 124 insertions(+), 41 deletions(-) diff --git a/cdist/argparse.py b/cdist/argparse.py index f390a974..bedb23ac 100644 --- a/cdist/argparse.py +++ b/cdist/argparse.py @@ -485,7 +485,7 @@ def get_parsers(): parser['scan'].add_argument( '-m', '--mode', help='Which modes should run', action='append', default=[], - choices=['scan', 'trigger']) + choices=['scan', 'trigger', 'config']) parser['scan'].add_argument( '--list', action='store_true', @@ -498,6 +498,10 @@ def get_parsers(): '-I', '--interfaces', action='append', default=[], required=True, help='On which interfaces to scan/trigger') + parser['scan'].add_argument( + '--name-mapper', + action='store', default=None, + help='Map addresses to names, required for config mode') parser['scan'].add_argument( '-d', '--delay', action='store', default=3600, type=int, diff --git a/cdist/scan/commandline.py b/cdist/scan/commandline.py index 331694e4..1a0ab0a7 100644 --- a/cdist/scan/commandline.py +++ b/cdist/scan/commandline.py @@ -37,7 +37,10 @@ def run(scan, args): log.debug("Trigger started") if 'scan' in args.mode: - s = scan.Scanner(interfaces=args.interfaces, args=args) + s = scan.Scanner( + autoconfigure='config' in args.mode, + interfaces=args.interfaces, + name_mapper=args.name_mapper) s.start() processes.append(s) log.debug("Scanner started") @@ -46,24 +49,27 @@ def run(scan, args): process.join() def list(scan, args): - s = scan.Scanner(interfaces=args.interfaces, args=args) + s = scan.Scanner(interfaces=args.interfaces, name_mapper=args.name_mapper) hosts = s.list() # A full IPv6 addresses id composed of 8 blocks of 4 hexa chars + # 6 colons. ipv6_max_size = 8 * 4 + 10 - # We format dates as follow: YYYY-MM-DD HH:MM:SS - date_max_size = 8 + 2 + 6 + 2 + date_max_size = len(datetime.now().strftime(scan.datetime_format)) + name_max_size = 25 - print("{} | {}".format( - 'link-local address'.ljust(ipv6_max_size), - 'last seen'.ljust(date_max_size))) - print('=' * (ipv6_max_size + 3 + date_max_size)) - for addr in hosts: - last_seen = datetime.strftime( - datetime.strptime(hosts[addr]['last_seen'].strip(), '%Y-%m-%d %H:%M:%S.%f'), - '%Y-%m-%d %H:%M:%S') - print("{} | {}".format(addr.ljust(ipv6_max_size),last_seen.ljust(date_max_size))) + print("{} | {} | {} | {}".format( + 'name'.ljust(name_max_size), + 'address'.ljust(ipv6_max_size), + 'last seen'.ljust(date_max_size), + 'last configured'.ljust(date_max_size))) + print('=' * (name_max_size + 3 + ipv6_max_size + 2 * (3 + date_max_size))) + for host in hosts: + print("{} | {} | {} | {}".format( + host.name(default='-').ljust(name_max_size), + host.address().ljust(ipv6_max_size), + host.last_seen().ljust(date_max_size), + host.last_configured().ljust(date_max_size))) # CLI processing is defined outside of the main scan class to handle # non-available optional scapy dependency (instead of crashing mid-flight). @@ -82,6 +88,11 @@ def commandline(args): # By default scan and trigger, but do not call any action. args.mode = ['scan', 'trigger', ] + if 'config' in args.mode and args.name_mapper == None: + print('--name-mapper must be specified for scanner config mode.', + file=sys.stderr) + sys.exit(1) + # Print known hosts and exit is --list is specified - do not start # the scanner. if args.list: diff --git a/cdist/scan/scan.py b/cdist/scan/scan.py index f3976370..459138e2 100644 --- a/cdist/scan/scan.py +++ b/cdist/scan/scan.py @@ -63,6 +63,69 @@ import cdist.config logging.basicConfig(level=logging.DEBUG) log = logging.getLogger("scan") +datetime_format = '%Y-%m-%d %H:%M:%S' + +class Host(object): + def __init__(self, addr, outdir, name_mapper=None): + self.addr = addr + self.workdir = os.path.join(outdir, addr) + self.name_mapper = name_mapper + + os.makedirs(self.workdir, exist_ok=True) + + def __get(self, key, default=None): + fname = os.path.join(self.workdir, key) + value=default + if os.path.isfile(fname): + with open(fname, "r") as fd: + value = fd.readline() + return value + + def __set(self, key, value): + fname = os.path.join(self.workdir, key) + with open(fname, "w") as fd: + fd.write(f"{value}") + + def name(self, default=None): + if self.name_mapper == None: + return default + + fpath = os.path.join(os.getcwd(), self.name_mapper) + if os.path.isfile(fpath) and os.access(fpath, os.X_OK): + out = subprocess.run([fpath, self.addr], capture_output=True) + if out.returncode != 0: + return default + else: + value = out.stdout.decode() + return (None if len(value) == 0 else value) + else: + return default + + def address(self): + return self.addr + + def last_seen(self, default=None): + raw = self.__get('last_seen') + if raw: + return datetime.datetime.strptime(raw, datetime_format) + else: + return default + + def last_configured(self, default=None): + raw = self.__get('last_configured') + if raw: + return datetime.datetime.strptime(raw, datetime_format) + else: + return default + + def seen(self): + now = datetime.datetime.now().strftime(datetime_format) + self.__set('last_seen', now) + + def configure(self): + # TODO: configure. + now = datetime.datetime.now().strftime(datetime_format) + self.__set('last_configured', now) class Trigger(object): """ @@ -108,48 +171,43 @@ class Scanner(object): Scan for replies of hosts, maintain the up-to-date database """ - def __init__(self, interfaces, args=None, outdir=None): + def __init__(self, interfaces, autoconfigure=False, outdir=None, name_mapper=None): self.interfaces = interfaces + self.autoconfigure=autoconfigure + self.name_mapper = name_mapper + self.config_delay = datetime.timedelta(seconds=3600) if outdir: self.outdir = outdir else: self.outdir = os.path.join(os.environ['HOME'], '.cdist', 'scan') + os.makedirs(self.outdir, exist_ok=True) + + self.running_configs = {} def handle_pkg(self, pkg): if ICMPv6EchoReply in pkg: - host = pkg['IPv6'].src - log.verbose("Host %s is alive", host) + host = Host(pkg['IPv6'].src, self.outdir, self.name_mapper) + if host.name(): + log.verbose("Host %s (%s) is alive", host.name(), host.address()) + else: + log.verbose("Host %s is alive", host.address()) + host.seen() - dir = os.path.join(self.outdir, host) - fname = os.path.join(dir, "last_seen") - - now = datetime.datetime.now() - - os.makedirs(dir, exist_ok=True) - - # FIXME: maybe adjust the format so we can easily parse again - with open(fname, "w") as fd: - fd.write(f"{now}\n") + # TODO check last config. + if self.autoconfigure and \ + host.last_configured(default=datetime.datetime.min) + self.config_delay < datetime.datetime.now(): + self.config(host) def list(self): - hosts = dict() - for linklocal_addr in os.listdir(self.outdir): - workdir = os.path.join(self.outdir, linklocal_addr) - # We ignore any (unexpected) file in this directory. - if os.path.isdir(workdir): - last_seen='-' - last_seen_file = os.path.join(workdir, 'last_seen') - if os.path.isfile(last_seen_file): - with open(last_seen_file, "r") as fd: - last_seen = fd.readline() - - hosts[linklocal_addr] = {'last_seen': last_seen} + hosts = [] + for addr in os.listdir(self.outdir): + hosts.append(Host(addr, self.outdir, self.name_mapper)) return hosts - def config(self): + def config(self, host): """ Configure a host @@ -158,9 +216,19 @@ class Scanner(object): - Maybe keep dict storing per host processes - Save the result - Save the output -> probably aligned to config mode - """ + if host.name() == None: + log.debug("config - could not resolve name for %s, aborting.", host.address()) + return + + if self.running_configs.get(host.name()) != None: + log.debug("config - is already running for %s, aborting.", host.name()) + + log.info("config - running against host %s.", host.name()) + p = host.configure() + self.running_configs[host.name()] = p + def start(self): self.process = Process(target=self.scan) self.process.start()