forked from ungleich-public/cdist
[scanner] add host class, name mapper and pre-config logic
This commit is contained in:
parent
bb24d632d6
commit
13e2ad175f
3 changed files with 124 additions and 41 deletions
|
@ -485,7 +485,7 @@ def get_parsers():
|
||||||
parser['scan'].add_argument(
|
parser['scan'].add_argument(
|
||||||
'-m', '--mode', help='Which modes should run',
|
'-m', '--mode', help='Which modes should run',
|
||||||
action='append', default=[],
|
action='append', default=[],
|
||||||
choices=['scan', 'trigger'])
|
choices=['scan', 'trigger', 'config'])
|
||||||
parser['scan'].add_argument(
|
parser['scan'].add_argument(
|
||||||
'--list',
|
'--list',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
|
@ -498,6 +498,10 @@ def get_parsers():
|
||||||
'-I', '--interfaces',
|
'-I', '--interfaces',
|
||||||
action='append', default=[], required=True,
|
action='append', default=[], required=True,
|
||||||
help='On which interfaces to scan/trigger')
|
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(
|
parser['scan'].add_argument(
|
||||||
'-d', '--delay',
|
'-d', '--delay',
|
||||||
action='store', default=3600, type=int,
|
action='store', default=3600, type=int,
|
||||||
|
|
|
@ -37,7 +37,10 @@ def run(scan, args):
|
||||||
log.debug("Trigger started")
|
log.debug("Trigger started")
|
||||||
|
|
||||||
if 'scan' in args.mode:
|
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()
|
s.start()
|
||||||
processes.append(s)
|
processes.append(s)
|
||||||
log.debug("Scanner started")
|
log.debug("Scanner started")
|
||||||
|
@ -46,24 +49,27 @@ def run(scan, args):
|
||||||
process.join()
|
process.join()
|
||||||
|
|
||||||
def list(scan, args):
|
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()
|
hosts = s.list()
|
||||||
|
|
||||||
# A full IPv6 addresses id composed of 8 blocks of 4 hexa chars +
|
# A full IPv6 addresses id composed of 8 blocks of 4 hexa chars +
|
||||||
# 6 colons.
|
# 6 colons.
|
||||||
ipv6_max_size = 8 * 4 + 10
|
ipv6_max_size = 8 * 4 + 10
|
||||||
# We format dates as follow: YYYY-MM-DD HH:MM:SS
|
date_max_size = len(datetime.now().strftime(scan.datetime_format))
|
||||||
date_max_size = 8 + 2 + 6 + 2
|
name_max_size = 25
|
||||||
|
|
||||||
print("{} | {}".format(
|
print("{} | {} | {} | {}".format(
|
||||||
'link-local address'.ljust(ipv6_max_size),
|
'name'.ljust(name_max_size),
|
||||||
'last seen'.ljust(date_max_size)))
|
'address'.ljust(ipv6_max_size),
|
||||||
print('=' * (ipv6_max_size + 3 + date_max_size))
|
'last seen'.ljust(date_max_size),
|
||||||
for addr in hosts:
|
'last configured'.ljust(date_max_size)))
|
||||||
last_seen = datetime.strftime(
|
print('=' * (name_max_size + 3 + ipv6_max_size + 2 * (3 + date_max_size)))
|
||||||
datetime.strptime(hosts[addr]['last_seen'].strip(), '%Y-%m-%d %H:%M:%S.%f'),
|
for host in hosts:
|
||||||
'%Y-%m-%d %H:%M:%S')
|
print("{} | {} | {} | {}".format(
|
||||||
print("{} | {}".format(addr.ljust(ipv6_max_size),last_seen.ljust(date_max_size)))
|
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
|
# CLI processing is defined outside of the main scan class to handle
|
||||||
# non-available optional scapy dependency (instead of crashing mid-flight).
|
# 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.
|
# By default scan and trigger, but do not call any action.
|
||||||
args.mode = ['scan', 'trigger', ]
|
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
|
# Print known hosts and exit is --list is specified - do not start
|
||||||
# the scanner.
|
# the scanner.
|
||||||
if args.list:
|
if args.list:
|
||||||
|
|
|
@ -63,6 +63,69 @@ import cdist.config
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
log = logging.getLogger("scan")
|
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):
|
class Trigger(object):
|
||||||
"""
|
"""
|
||||||
|
@ -108,48 +171,43 @@ class Scanner(object):
|
||||||
Scan for replies of hosts, maintain the up-to-date database
|
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.interfaces = interfaces
|
||||||
|
self.autoconfigure=autoconfigure
|
||||||
|
self.name_mapper = name_mapper
|
||||||
|
self.config_delay = datetime.timedelta(seconds=3600)
|
||||||
|
|
||||||
if outdir:
|
if outdir:
|
||||||
self.outdir = outdir
|
self.outdir = outdir
|
||||||
else:
|
else:
|
||||||
self.outdir = os.path.join(os.environ['HOME'], '.cdist', 'scan')
|
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):
|
def handle_pkg(self, pkg):
|
||||||
if ICMPv6EchoReply in pkg:
|
if ICMPv6EchoReply in pkg:
|
||||||
host = pkg['IPv6'].src
|
host = Host(pkg['IPv6'].src, self.outdir, self.name_mapper)
|
||||||
log.verbose("Host %s is alive", host)
|
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)
|
# TODO check last config.
|
||||||
fname = os.path.join(dir, "last_seen")
|
if self.autoconfigure and \
|
||||||
|
host.last_configured(default=datetime.datetime.min) + self.config_delay < datetime.datetime.now():
|
||||||
now = datetime.datetime.now()
|
self.config(host)
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
def list(self):
|
def list(self):
|
||||||
hosts = dict()
|
hosts = []
|
||||||
for linklocal_addr in os.listdir(self.outdir):
|
for addr in os.listdir(self.outdir):
|
||||||
workdir = os.path.join(self.outdir, linklocal_addr)
|
hosts.append(Host(addr, self.outdir, self.name_mapper))
|
||||||
# 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}
|
|
||||||
|
|
||||||
return hosts
|
return hosts
|
||||||
|
|
||||||
|
|
||||||
def config(self):
|
def config(self, host):
|
||||||
"""
|
"""
|
||||||
Configure a host
|
Configure a host
|
||||||
|
|
||||||
|
@ -158,9 +216,19 @@ class Scanner(object):
|
||||||
- Maybe keep dict storing per host processes
|
- Maybe keep dict storing per host processes
|
||||||
- Save the result
|
- Save the result
|
||||||
- Save the output -> probably aligned to config mode
|
- 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):
|
def start(self):
|
||||||
self.process = Process(target=self.scan)
|
self.process = Process(target=self.scan)
|
||||||
self.process.start()
|
self.process.start()
|
||||||
|
|
Loading…
Reference in a new issue