#! /usr/bin/python3

import sys
import os
import re
import json
from datetime import datetime, timedelta, timezone

# Usually this script will be in $HOME/bin, and libtiny.py
# is in $HOME/lib.
# Update sys.path (PYTHONPATH) to include the lib directory.
# Typically $HOME/bin already is in PATH. $HOME/lib is not
# always in PYTHONPATH, so add it if necessary.
bindir = os.path.join( os.path.dirname(__file__) )
topdir = os.path.realpath( os.path.join( bindir, '..') )
libdir = os.path.join( topdir, 'lib' )
etcdir = os.path.join( topdir, 'etc' )
if not libdir in sys.path:
	sys.path.insert(0,libdir)

from libarkmon					import DB
from libarkmon.libtiny			import SAY, get_asn, ASrank
from libarkmon.libarkmon_helper	import validate_attrs, expand_attrs, attr_data

#+
# NAME:
#	arkmon.py
# PURPOSE:
#	Provides access to the ArkMon postgres state and history tables
# CALLING SEQUENCE:
# OPTIONAL INPUTS:
# RESTRICTIONS:
# PROCEDURE:
# MODIFICATION HISTORY:
#	FEB-2025, Paul Hick (UCSD/CAIDA)
#-

TIME_REGEX = re.compile(r'^((?P<days>[\.\d]+?)D)?((?P<hours>[\.\d]+?)h)?((?P<minutes>[\.\d]+?)m)?((?P<seconds>[\.\d]+?)s)?$')
# recreate what the old eon library tries to do with split_delta_format,
# and use the same time period labels that it expects:
# (D)ays, (h)ours, (m)inutes, (s)econds
def str_to_timedelta(timestr):
	parts = TIME_REGEX.match(timestr)
	if parts is None:
		print(sys.stderr, "Example time format 1D2h3m4s, all parts optional")
		sys.exit(1)
	params = {name: float(param) for name, param in parts.groupdict().items() if param}
	return timedelta(**params)

# timedelta normalizes components so that only days can be negative, but
# that doesn't look right. Microseconds also get in the way when printing.
# Make all the deltas positive, with zero microseconds so they print nicely
def timedelta_to_str(delta):
    delta = timedelta(days=delta.days, seconds=delta.seconds)
    return str(abs(delta))


def arkmon(all_nodes,options_list,db=None,live_status=None):

	default_options = {
		'verbose'		: 0		,
		'user'			: 'arkmon-dev',
		'db'			: 'arkmon-dev',
		'attribute'		: None	,
		'short'			: False	,
		'dump'			: None	,
		'separator'		: '\n'	,
		'start_time'	: None	,
		'stop_time'		: None	,
		'posix'			: False	,
		'degree'		: 0		,
		'array'			: False	,
		'hash'			: False	,
		'json'			: False	,
		'yaml'			: False	,
		'all_values'	: False	,
		'alive'			: None	,
		'count_only'	: False	,
		'names_only'	: False ,
		'time_only'		: False	,
		'value_only'	: False	,
		'above'			: None	,
		'below'			: None	,
		'equal'			: None	,
		'unequal'		: None	,
		'verify_date'	: False	,
	}

	say = SAY('arkmon')

	# Replace default options by options in input

	options = default_options
	for k in list(options_list.keys()):
		options[k] = options_list[k]

	# If db was input, then do not close.
	# If opening db here, then close it before returning

	if db:
		close_db = False
	else:
		db = DB( user=options['user'], name=options['db'] )
		close_db = True

	if options['dump']:
		options['attribute'] = options['dump']
		options['dump'] = True

	if options['attribute']:
		attrs = options['attribute'].split(',')
		attrs = expand_attrs( attrs, short=options['short'], exit=True )
		validate_attrs( attrs )
	else:
		if close_db:
			db.attr_sublist = [ k for k in db.attr_in_db if k in db.attr_data and db.attr_data[k]['degr'] <= options['degree'] ]
		attrs = db.attr_sublist

	attrs.sort()

	start_time = options['start_time']
	stop_time  = options['stop_time' ]

	# this is documented to be YYYY-MM, so only allow that formatting
	if start_time is not None:
		start_time = datetime.strptime(start_time, "%Y-%m").replace(tzinfo=timezone.utc).timestamp()
	if stop_time is not None:
		stop_time = datetime.strptime(stop_time, "%Y-%m").replace(tzinfo=timezone.utc).timestamp()

	if not live_status:
		live_status = { node: True for node in all_nodes }

	status = 0
	nodes = {};

	for attr in attrs:

		# -a Utils=bdr:tsp
		# -a ActivityDirs=vrfinder:topo-v6

		attribute = attr.split('=')
		selection = attribute[1].split(':') if len(attribute) > 1 else []
		attribute = attribute[0]

		for node in all_nodes:

			if options['dump']:

				rows = db.dump( node, attribute, start_time, stop_time, sort='asc' )

				if not rows:
					say.say( "no data for node '%s'"%node )
					status += 1
					continue

				mtype = db.attr_data[attribute]['math']
				if mtype == 'integer':
					if options['above']:
						vv = int(options['above'])
						rows = [ row for row in rows if int(row[1]) >= vv ]
					if options['below']:
						vv = int(options['below'])
						rows = [ row for row in rows if int(row[1]) <= vv ]
					if options['equal']:
						vv = int(options['equal'])
						rows = [ row for row in rows if int(row[1]) == vv ]
				elif mtype == 'float':
					if options['above']:
						vv = float(options['above'])
						rows = [ row for row in rows if float(row[1]) >= vv ]
					if options['below']:
						vv = float(options['below'])
						rows = [ row for row in rows if float(row[1]) <= vv ]
					if options['equal']:
						vv = float(options['equal'])
						rows = [ row for row in rows if float(row[1]) == vv ]
				elif mtype == 'bool':
					if options['equal']:
						vv = int(options['equal'  ])
						rows = [ row for row in rows if int(row[1]) == vv ]
					if options['unequal']:
						vv = int(options['unequal'  ])
						rows = [ row for row in rows if int(row[1]) != vv ]
				elif mtype == 'ipv4':
					if options['equal']:
						vv = options['equal'  ]
						rows = [ row for row in rows if not row[1].startswith(vv) ]
					if options['unequal']:
						vv = options['unequal'  ]
						rows = [ row for row in rows if		row[1].startswith(vv) ]
				elif mtype == 'ipv6':
					if options['equal']:
						vv = options['equal'  ]
						rows = [ row for row in rows if not row[1].startswith(vv) ]
					if options['unequal']:
						vv = options['unequal'  ]
						rows = [ row for row in rows if		row[1].startswith(vv) ]
				elif mtype == 'string':
					if options['equal']:
						vv = options['equal'  ]
						rows = [ row for row in rows if vv     in row[1] ]
					if options['unequal']:
						vv = options['unequal'  ]
						rows = [ row for row in rows if vv not in row[1] ]

				if not rows:
					continue

				if not options['posix']:
					if attribute == 'Uptime':
						rows = [ (datetime.fromtimestamp(row[0], timezone.utc).strftime('%Y-%m-%dT%H:%M:%S'), str(timedelta(seconds=round(float(row[1])))) + datetime.fromtimestamp(round(float(row[0])-float(row[1])), timezone.utc).strftime(' (%Y-%m-%dT%H:%M:%S)')) for row in rows ]
					else:
						rows = [ (datetime.fromtimestamp(row[0]).strftime('%Y-%m-%dT%H:%M:%S'), row[1]) for row in rows ]

				if say.VERBOSE:
					if attribute == 'IPv4Global':
						print ("# %s: %s"%(node,attribute))
						for row in rows:
							asn = get_asn( row[1] )
							name = "Unknown" if asn == 0 else ASrank(asn)['organization']['orgName']								
							print ('%s %15s %7d %-s'%(row[0],row[1],asn,name))
					else:
						print ("# %s: %s"%(node,attribute))
						for row in rows:
							print ('%s %s'%(row[0],row[1]))
				else:
					print ("# %s: %s"%(node,attribute))
					for row in rows:
						print ('%s %s'%(row[0],row[1]))

			else:

				alive = ' ' if live_status[node] else '*'

				tt, vv = db.get_attr_for_node( node, attribute, get_pair=True )
				if options['verify_date']:

					# If --verify-date is set, then pick up time the value was last verified
					# i.e. the time when the last packet arrived that contained the information.

					pkt_type = db.attr_data[attribute]['pkts']	# Pkt type containing information
					if pkt_type:
						tv = tt		# Could be None
						for p in pkt_type:
							tt = db.get_attr_for_node( node, 'pkttype', value=str(p), get_time=True )
							if tv is None:
								if tt is not None:
									tv = tt
							else:
								if tt is not None and tv > tt:
									tv = tt
						tt = tv
					else:					# ipv4global and hostname are in all packets
						tt = db.get_attr_for_node( node, 'uptime', get_time=True )

				if tt == None:				# Node not in table
					if not (options['array'] or options['hash'] or options['json'] or options['yaml']) and options['all_values']:
						print ('%-7s%s %-15s None'%(node,alive,attribute))
					status += 1
					continue

				if not ( (start_time == None or start_time <= tt) and (stop_time == None or tt < stop_time) ):
					status += 1
					continue

				type = db.attr_data[attr]['math']
				if	(type == 'integer' and (	\
						(options['above'  ] and int  (vv) <= int  (options['above'  ]))	or	\
						(options['below'  ] and int  (vv) >= int  (options['below'  ]))	or	\
						(options['equal'  ] and int  (vv) != int  (options['equal'  ]))	or	\
						(options['unequal'] and int  (vv) == int  (options['unequal']))		\
					))	or	\
					(type == 'float'   and (	\
						(options['above'  ] and float(vv) <= float(options['above'  ]))	or	\
						(options['below'  ] and float(vv) >= float(options['below'  ]))	or	\
						(options['equal'  ] and float(vv) != float(options['equal'  ]))	or	\
						(options['unequal'] and float(vv) == float(options['unequal']))		\
					))	or	\
					(type == 'bool'	   and (	\
						(options['equal'  ] and int  (vv) != float(options['equal'  ]))	or	\
						(options['unequal'] and int  (vv) == float(options['unequal']))		\
					))	or	\
					(type == 'ipv4'	   and (	\
						(options['equal'  ] and not vv.startswith(options['equal'  ]))	or	\
						(options['unequal'] and		vv.startswith(options['unequal']))		\
					))	or	\
					(type == 'ipv6'	   and (	\
						(options['equal'  ] and not vv.startswith(options['equal'  ]))	or	\
						(options['unequal'] and		vv.startswith(options['unequal']))		\
					))	or	\
					(type == 'string'	    and (	\
						(options['equal'  ] and options['equal'  ] not in vv )	or	\
						(options['unequal'] and options['unequal']     in vv )		\
					)):
					status += 1
					continue

				if options['array'] or options['hash'] or options['json'] or options['yaml'] or options['count_only'] or options['names_only']:
					if node not in nodes:
						nodes[node] = {}
						if options['alive']:
							nodes[node]['_alive_'] = 'true' if live_status[node] else 'false'
						nodes[node]['_name_' ] = node

					#nodes[node][attr] = None

				if tt and not options['posix']:
					tt = datetime.fromtimestamp(tt, timezone.utc)
					if attribute == 'Uptime':
						vv = timedelta(seconds=round(float(vv)))
						vv = str(vv) + (tt-vv).strftime(' (%Y-%m-%dT%H:%M:%S)' )
					tt = tt.strftime('%Y-%m-%dT%H:%M:%S') + ' (' + timedelta_to_str(tt-datetime.now(timezone.utc)) + ')'

				if not vv:
					status += 1
					continue

				if options['array'] or options['hash'] or options['json'] or options['yaml'] or options['count_only'] or options['names_only']:
					nodes[node][attribute] = {'value': vv, 'ut': tt}
				elif options['value_only']:
					print (node+alive+':'+attribute+'='+vv)
				elif options['time_only']:
					print (node+alive+':'+('posix='+str(tt) if options['posix'] else 'UT='+tt))
				else:
					print ('%-7s%s %-15s %-40s %s'%(node,alive,attribute,vv,tt))
					if say.VERBOSE:
						if attribute == 'ASN':
							asorg = ASrank( int(vv) )['organization']['orgName']
							if asorg:
								print ( ' '*8+asorg)
						elif attribute == "SDManfID":
							if vv in db.attr_sdcard:
								print ( ' '*9+db.attr_sdcard[vv]['brand'])

	if close_db:
		db.close()
		db = None

	if options['count_only']:
		nodes = len(nodes)
	elif options['names_only']:
		nodes = list(nodes)
	elif options['array']:
		for node in nodes:
			nodes[node].pop('_name_')
		nodes = [ nodes[node] for node in nodes ]
	elif options['yaml']:
		rows = []
		for node in nodes:
			rows.append('---')
			rows.append('.monitor: %s'%node)
			for attr in nodes[node]:
				rows.append('%s:%s'%(attr,nodes[node][attr]))
		nodes = rows
	else:		# nodes already is a hash (dict)
		pass
	
	if options['json']:
		nodes = json.dumps( nodes )

	return (status,nodes)

if __name__ == '__main__':

	from optparse import OptionParser

	version = '3.10'
	usage = "%prog --attribute=ATTRIBUTE NODE1 NODE2 ...\n\n"					+ \
		"Specify nodes on the command line (if omitted all nodes are used)\n"	+ \
		"By default, the last known value of an attribute is printed;\n"		+ \
		"%prog --dump prints all changes over time\n\n"							+ \
		"See %prog --help and %prog --list-attributes\n"						+ \
		"Examples:\n"	+ \
		"  %prog --?a[ttributes]                          list of attributes\n"									+ \
		"  %prog --?db                                    database info\n"										+ \
		"  %prog san-us                                   all properties of san-us\n"							+ \
		"  %prog --attr hwmac                             list hwmac, sort by node name\n"						+ \
		"  %prog --attr hwmac --sort default              list hwmac, sort by hwmac\n"							+ \
		"  %prog --attr hwmac --sort ipv4local            list hwmac, sort by ipv4local\n"						+ \
		"  %prog --attr ipv4global san-us                 global IPv4 address of san-us\n"						+ \
		"  %prog --attr osname --equal bullseye           all nodes running bullseye\n"							+ \
		"  %prog --attr oskernel --equal osname=bullseye  kernel version for nodes running bullseye\n"			+ \
		"  %prog --attr ipv4global --unequal activities=ipv4-probing\n"											+ \
		"                                                 IPv6 addresses for nodes not running ipv6-probing\n"	+ \
		"  %prog --find status=Inactive                   list inactive nodes\n"								+ \
		"  %prog --find has-no-hwmac                      list nodes that have no mac set\n"					+ \
		"  %prog --match status=Inactive --attr hwmac     list hwmac for inactive nodes"						+ \
		""

	parser = OptionParser(usage=usage,version=version)

	default_db   = 'arkmon-dev'
	default_user = 'arkmon-dev'
	default_attr_names = sorted( list( attr_data() ) )


	parser.add_option('-v', '--verbose'	,
		dest		= 'verbose'			,
		action		= 'count'			,
		default		= 0					,
		help		= 'verbose output'	,
	)

	parser.add_option('-n', '--dry-run'	,
		dest		= 'dryrun'			,
		action		= 'store_true'		,
		default		= False				,
		help		= 'make dryrun'		,
	)

	parser.add_option('', '--db'		,
		dest		= 'db'				,
		action		= 'store'			,
		default		= default_db		,
		help		= 'db name on ghul.caida.org, default: %s'%default_db,
	)

	parser.add_option('', '--?db'		,
		dest		= 'what_db'			,
		action		= 'store_true'		,
		default		= False				,
		help		= 'print postgres db information',
	)

	parser.add_option('', '--user'		,
		dest		= 'user'			,
		action		= 'store'			,
		default		= default_user		,
		help		= 'db user on ghul.caida.org, default: %s'%default_user,
	)

	parser.add_option('', '--?attributes',
		dest		= 'list_attributes'	,
		action		= 'store_true'		,
		default		= False				,
		help		= 'list all node attributes',
	)

	parser.add_option('-a', '--attribute',
		dest		= 'attribute'		,
		action		= 'store'			,
		default		= None				,
		help		= 'latest value of attributes in comma-separated list of %s'%'|'.join(default_attr_names),
	)

	parser.add_option('', '--short'		,
		dest		= 'short'			,
		action		= 'store_true'		,
		default		= False				,
		help		= 'allow abbreviated attr values',
	)

	parser.add_option('-d', '--dump'	,
		dest		= 'dump'			,
		action		= 'store'			,
		default		= None				,
		help		= 'dump attributes (see --attribute)',
	)

	parser.add_option('-s', '--sort'	,
		dest		= 'sort'			,
		action		= 'store'			,
		default		= None				,
		help		= 'sort by specified attribute',
	)

	parser.add_option('-r', '--reverse'	,
		dest		= 'reverse'			,
		action		= 'store_true'		,
		default		= False				,
		help		= 'reverse sort when --sort is used',
	)

	parser.add_option('', '--time-sort'	,
		dest		= 'time_sort'		,
		action		= 'store_true'		,
		default		= False				,
		help		= 'sort by timestamp',
	)

	parser.add_option('-p', '--posix'	,
		dest		= 'posix'			,
		action		= 'store_true'		,
		default		= False				,
		help		= 'print posix times (instead of dates)',
	)

	parser.add_option('', '--separator'	,
		dest		= 'separator'		,
		action		= 'store'			,
		default		= '\n'				,
		help		= 'separator for printing list of node names',
	)

	parser.add_option('', '--start-time',
		dest		= 'start_time'		,
		action		= 'store'			,
		type		= 'string'			,
		default		= None				,
		help		= 'show data after start date YYYY-MM'
	)

	parser.add_option('', '--stop-time'	,
		dest		= 'stop_time'		,
		action		= 'store'			,
		type		= 'string'			,
		default		= None				,
		help		= 'show data before stop date YYYY-MM'
	)

	parser.add_option('', '--alive'		,
		dest		= 'alive'			,
		action		= 'store'			,
		type		= 'string'			,
		default		= None				,
		help		= 'show "live" nodes that pinged home less than delta-time ago (secs or .D.h.m.s)',
	)

	parser.add_option('', '--show-dead'	,
		dest		= 'show_dead'		,
		action		= 'store_true'		,
		default		= False				,
		help		= 'list names of dead monitors with --alive set',
	)

	parser.add_option('','--history'	,
		dest		= 'history'			,
		action		= 'store_true'		,
		default		= False				,
		help		= 'use history (incl all nodes that ever existed)',
	)

	parser.add_option('', '--full-count',
		dest		= 'full_count'		,
		action		= 'store_true'		,
		default		= False				,
		help		= 'print node count for all nodes in db',
	)

	parser.add_option('', '--all-names'	,
		dest		= 'all_names'		,
		action		= 'store_true'		,
		default		= False				,
		help		= 'print all node names in db',
	)

	parser.add_option('', '--count-only',
		dest		= 'count_only'		,
		action		= 'store_true'		,
		default		= False				,
		help		= 'show counts only with --alive set',
	)

	parser.add_option('', '--names-only',
		dest		= 'names_only'		,
		action		= 'store_true'		,
		default		= False				,
		help		= 'only print node names',
	)

	parser.add_option('', '--time-only'	,
		dest		= 'time_only'		,
		action		= 'store_true'		,
		default		= False				,
		help		= 'show timestamp only'	,
	)

	parser.add_option('', '--value-only',
		dest		= 'value_only'		,
		action		= 'store_true'		,
		default		= False				,
		help		= 'show attribute value only',
	)

	parser.add_option('', '--degree'	,
		dest		= 'degree'			,
		action		= 'store'			,
		type		= 'int'				,
		default		= 0					,
		help		= 'incl attributes with degree below setting only',
	)

	parser.add_option('', '--all-values',
		dest		= 'all_values'		,
		action		= 'store_true'		,
		default		= False				,
		help		= 'include None values'	,
	)

	parser.add_option('-f', '--find'	,
		dest		= 'find'			,
		action		= 'store'			,
		type		= 'string'			,
		default		= None				,
		help		= 'find matching nodes/values (attr=value, is[-not]-attr, has[-not]-attr, was[-not]-attr, had[-not]-attr)',
	)

	parser.add_option('-m', '--match'	,
		dest		= 'match'			,
		action		= 'store'			,
		type		= 'string'			,
		default		= None				,
		help		= 'select matching nodes (attr=value, is[-not]-attr, has[-not]-attr, was[-not]-attr, had[-not]-attr)',
	)

	parser.add_option('', '--array'		,
		dest		= 'array'			,
		action		= 'store_true'		,
		default		= False				,
		help		= 'print node info as array',
	)

	parser.add_option('', '--hash'		,
		dest		= 'hash'			,
		action		= 'store_true'		,
		default		= False				,
		help		= 'print node info as hash',
	)

	parser.add_option('','--json'		,
		dest		= 'json'			,
		action		= 'store_true'		,
		default		= False				,
		help		= 'convert to json'	,
	)

	parser.add_option('','--yaml'		,
		dest		= 'yaml'			,
		action		= 'store_true'		,
		default		= False				,
		help		= 'convert to old monitors.yaml format',
	)

	parser.add_option('', '--above'		,
		dest		= 'above'			,
		action		= 'store'			,
		type		= 'string'			,
		default		= None				,
		help		= 'list values above',
	)

	parser.add_option('', '--below'		,
		dest		= 'below'			,
		action		= 'store'			,
		type		= 'string'			,
		default		= None				,
		help		= 'list values below',
	)

	parser.add_option('', '--equal'		,
		dest		= 'equal'			,
		action		= 'store'			,
		type		= 'string'			,
		default		= None				,
		help		= 'list values equal',
	)

	parser.add_option('', '--unequal'	,
		dest		= 'unequal'			,
		action		= 'store'			,
		type		= 'string'			,
		default		= None				,
		help		= 'list values unequal',
	)

	parser.add_option('', '--verify-date',
		dest		= 'verify_date'		,
		action		= 'store_true'		,
		default		= False				,
		help		= 'show time data were last verified',
	)

	options, args = parser.parse_args()

	say = SAY(
		label   = 'arkmon-main'		,
		verbose = options.verbose	,
		dryrun  = options.dryrun                                                                        ,
	)

	db = DB( user=options.user, name=options.db )
	if not db.open:
		say.die( 'failed to open db "options.db"')

	if options.what_db:
		say.yell ( db.message )
		sys.exit()

	db.load_attr_data()

	#groups = ["All", "Contacts", "Device", "IP", "Location", "Perform", "Storage", "VP"]

	db.attr_sublist = [ k for k in db.attr_in_db if k in db.attr_data and db.attr_data[k]['degr'] <= options.degree ]

	if options.list_attributes:
		say.yell('\n'+'\n'.join( [ '%-15s  %s'%(k, db.attr_data[k]['desc']) for k in db.attr_sublist ] ))
		sys.exit()

	#==========================
	# Get a list of nodes
	#	-- from database
	#	-- from stdout
	#	-- from argument list

	#state = not ( options.history or options.find or options.match or options.dump or args )
	state = not ( options.history or options.dump or args )
	nodes = set(db.nodes( state=state ))

	if len(args) == 0:
		live_nodes = nodes
	else:
		live_nodes = set( sys.stdin.read().split('\n')[0:-1] if len(args) == 1 and args[0] == '-' else args )
		nodes = live_nodes-nodes
		if nodes:
			say.say( "unrecognized node(s) excluded: %s"%','.join(nodes) )
			live_nodes -= nodes

	if len(live_nodes) == 0:
		sys.exit()
	live_nodes = list(live_nodes)
	live_nodes.sort()

	#==========================

	alive = False
	if options.alive != None:
		alive = True				#D:h:m:s
		if '-' in options.alive:
			alive_start_time, alive_stop_time = options.alive.split('-')
		else:
			alive_start_time = options.alive
			alive_stop_time  = None

		if alive_start_time.isdigit():
			alive_start_time = timedelta(seconds=int(alive_start_time))
		else:
			alive_start_time = str_to_timedelta(alive_start_time)
		say.say( "alive if last ping less than %s ago"%alive_start_time )
		alive_start_time = (datetime.now(timezone.utc)-alive_start_time).timestamp()

		if alive_stop_time:
			if alive_stop_time.isdigit():
				alive_stop_time = timedelta(seconds=int(alive_stop_time))
			else:
				alive_stop_time = str_to_timedelta(alive_stop_time)
			say.say( "alive if last ping more than %s ago"%alive_stop_time )
			alive_stop_time  = (datetime.now(timezone.utc)-alive_stop_time).timestamp()

		nodes = live_nodes
		live_nodes = []
		dead_nodes = []
		for node in nodes:
			tt = db.get_attr_for_node( node, 'pkttype', get_time=True )
			if tt == None or (alive_start_time and tt < alive_start_time) or (alive_stop_time and tt > alive_stop_time):
				dead_nodes.append(node)
			else:
				live_nodes.append(node)
	else:
		dead_nodes = []

	if options.sort is not None:
		opt_sort = options.sort
		if opt_sort.startswith('def') and options.attribute:
			opt_sort = options.attribute.split(',')[0]
		
		live_nodes.sort( key=lambda node: db.sort_fnc(node, opt_sort, options.time_sort), reverse=options.reverse )
		dead_nodes.sort( key=lambda node: db.sort_fnc(node, opt_sort, options.time_sort), reverse=options.reverse )

	if options.full_count:
		print ("%d"%len(live_nodes)+(",%d"%len(dead_nodes) if options.show_dead else ''))
		sys.exit()

	if options.all_names:
		nodes = options.separator.join(live_nodes)+(('\n' if len(live_nodes) > 0 else '')+options.separator.join([x+'*' for x in dead_nodes]) if options.show_dead and len(dead_nodes) > 0 else '')
		if nodes:
			print (nodes)
		sys.exit()

	all_nodes   = live_nodes
	live_status = { node: True for node in live_nodes }
	if options.show_dead:
		all_nodes.extend(dead_nodes)
		live_status.update( { node: False for node in dead_nodes } )

	# Not sure this is still useful (same functionality is covered with --equal??)
	# --find is[-not]-attr		only applied to boolean attrs; only searches state db
	# --find was[-not]-attr		only applied to boolean attrs; also searches history
	# --find has[-not]-attr		only searches state db
	# --find had[-not]-attr		also searches history
	# --find attr=value			searches state and history

	if options.find:
		tt_format = '%Y-%m-%dT%H:%M:%S'
		say.say( 'find: %s'%options.find )
		result = db.nodes_match( options.find, nodes=all_nodes )

		# Output is a list of nodes if names_only=True, or
		# find has a 'has-','is-','has-not','is-not' part
		if isinstance(result,dict):
			for node in result:
				for attr in result[node]:
					tt, vv = result[node][attr]
					if not options.posix:
						tt = datetime.fromtimestamp(tt, timezone.utc).strftime(tt_format)
					print ("%8s: attribute=%-12s time=%s value=%s"%(node, attr, tt, vv))
		elif result:
			print( options.separator.join(result) )

		sys.exit()

	if options.match:
		say.say( 'match: %s'%options.match )
		all_nodes = db.nodes_match( options.match, nodes=all_nodes, names_only=True )

	opt = vars( options )

	# This picks up options like --attribute uptime --equal osname=bullseye
	# First nodes with OSname=bullseye are selected.
	# For the matching nodes the Uptime is returned.

	for math in ['above', 'below', 'equal','unequal']:
		if not (opt[math] and '=' in opt[math]):
			continue

		say.say( 'math: %s'%opt[math] )

		in_attr  = opt['attribute']							# Save input for later
		in_array = opt['array']
		in_hash  = opt['hash' ]

		opt['attribute'], opt[math] = opt[math].split('=')	# Set math option for processing
		opt['array'    ] = False
		opt['hash'     ] = True

		status, nodes = arkmon(all_nodes,opt,db,live_status)# Find matching nodes

		if nodes:
			all_nodes   = [ x for x in all_nodes if x in nodes ]
			live_status = { node: live_status[node] for node in all_nodes }
			opt[math] = None								# Clear math option again
			opt['attribute'] = in_attr						# Reset save input options
			opt['array'    ] = in_array
			opt['hash'     ] = in_hash
			status, nodes = arkmon(all_nodes,opt,db,live_status)

		break						# Only a single math option is processed			
	else:							# If no funky math options are used
		status, nodes = arkmon(all_nodes,opt,db,live_status)

	if nodes:
		print (nodes)

	sys.exit(0)

