mirror of
https://pagure.io/fedora-infra/ansible.git
synced 2026-03-19 19:46:38 +08:00
2396 lines
76 KiB
Python
Executable File
2396 lines
76 KiB
Python
Executable File
#! /usr/bin/python3
|
|
|
|
# Create/view a "txt" file using the ansible playbook
|
|
# "generate-updates-uptimes-per-host-file.yml" which records the number of rpms
|
|
# available to be upgraded for ansible hosts, and the uptime of those hosts.
|
|
# This is very helpful when doing upgrade+reboot runs, as we can easily see
|
|
# what has/hasn't been upgraded and/or rebooted.
|
|
|
|
# For examples, see the help command.
|
|
|
|
import os
|
|
import sys
|
|
|
|
import argparse
|
|
import fnmatch
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import time
|
|
|
|
# Use utf8 prefixes in diff, these need to be a "normal" width 1 character
|
|
conf_utf8 = True
|
|
_conf_utf8_boot_ed = '⚠' # Rebooted
|
|
_conf_utf8_boot_up = '⚐' # Rebooted and updated
|
|
_conf_utf8_more_up = '➚'
|
|
_conf_utf8_less_up = '➘'
|
|
_conf_utf8_diff_os = '➜' # '♺' OSinfo is different, but the machine is the same
|
|
_conf_utf8_diff_hw = '⇉' # '모' machine_id is different
|
|
|
|
# Arrow seperator for host command. Doesn't need to be a single character
|
|
conf_host_arrow_asci = '->'
|
|
conf_host_arrow_utf8 = '→'
|
|
|
|
# Use ansi codes. None means auto, aka. look for a tty on stdout.
|
|
conf_ansi_terminal = None
|
|
conf_term_cmd = 'dim'
|
|
conf_term_diffstat_hostnum = ''
|
|
conf_term_diffstat_updates = 'bold'
|
|
conf_term_diffstat_added = ''
|
|
conf_term_diffstat_instl = ''
|
|
conf_term_diffstat_boots = 'underline'
|
|
conf_term_highlight = 'bold,underline'
|
|
conf_term_host_boot_ed = 'fg:d,bold,underline'
|
|
conf_term_host_boot_up = 'bold'
|
|
conf_term_host_more_up = ''
|
|
conf_term_host_less_up = ''
|
|
conf_term_host_diff_os = 'bold'
|
|
conf_term_host_diff_hw = 'reverse'
|
|
conf_term_keyword = 'underline'
|
|
conf_term_time = 'underline'
|
|
conf_term_title = 'italic'
|
|
|
|
# Use _ instead of , for number seperator.
|
|
conf_num_sep_ = False
|
|
|
|
# This is kind of a hack, if you run from a cron job then it should run at
|
|
# the same time each day, and this should be 1 hour or less. But life isn't
|
|
# perfect, so we give it some more time.
|
|
# The two competing problems are 1) reboot machine with low uptime.
|
|
# 2) get data at 23:59 yesterday and 0:01 today.
|
|
# ...and we can't fix both without boot_id's ... so just have those.
|
|
conf_tmdiff_fudge = (60*60*4)
|
|
|
|
# How many hosts to show in tier 4 updates/uptimes...
|
|
conf_stat_4_hosts = 4
|
|
|
|
# Skip when everything is blank.
|
|
conf_host_skip_eq = True
|
|
|
|
# How many host matches before we show diffstat in "host" command.
|
|
conf_host_show_diffstat_hostnum = 8
|
|
|
|
# How many total host matches before we end host cmd early (-v shows all).
|
|
conf_host_end_total_hostnum = 20
|
|
|
|
# How many history files do we show by default (-v shows all).
|
|
conf_hist_show = 20
|
|
|
|
# Make it easier to see different date's
|
|
conf_ui_date = True
|
|
|
|
# Do we use a shorter duration by default (drop minutes/seconds)
|
|
conf_short_duration = True
|
|
|
|
# Do we want a small osinfo in diff/list/etc.
|
|
conf_small_osinfo = True
|
|
|
|
# Show machine/boot id's in info command, by default (use -v).
|
|
conf_info_machine_ids = False
|
|
|
|
# Try to print OS/ver even nicer (when small) ... but includes spaces.
|
|
conf_align_osinfo_small = True
|
|
|
|
# Allow 9,999,999 updates, or try to work out the correct size.
|
|
conf_fast_width_history = True
|
|
|
|
# Dynamically change the uptime of hosts based on the time since we looked
|
|
# at their uptime. Only for the main file. Assume they are still up etc.
|
|
conf_dynamic_main_uptime = True
|
|
|
|
# How old to consider when machines were created.
|
|
conf_created_dur_def = 0 # Forever
|
|
|
|
# Hosts that we'll show info. for, by default. info/host cmds.
|
|
conf_important_hosts = ["batcave*", "bastion01*", "noc*"]
|
|
|
|
# Remove suffix noise in names.
|
|
conf_suffix_dns_replace = {
|
|
'.fedoraproject.org' : '.<FP>.org',
|
|
'.fedorainfracloud.org' : '.<FitC>.org',
|
|
}
|
|
_suffix_dns_replace = {}
|
|
|
|
# How many days of history do we keep. Note that 1 is the minimum, as today
|
|
# has to be kept around with the main file.
|
|
conf_history_keep_def = 16
|
|
|
|
# Dir. where we put, and look for, the files...
|
|
conf_path = "/var/log/"
|
|
|
|
conf_user_conf_path = "~/.config/updates-uptimes/config.conf"
|
|
|
|
# Now we can change the above conf_ variables, via. a conf file.
|
|
def _user_conf():
|
|
ucp = os.path.expanduser(conf_user_conf_path)
|
|
if not os.path.exists(ucp):
|
|
return
|
|
|
|
for line in open(ucp):
|
|
_user_conf_line(line)
|
|
|
|
def _user_conf_line(line):
|
|
line = line.lstrip()
|
|
if not line: return
|
|
if line[0] == '#': return
|
|
|
|
op = "+="
|
|
x = line.split(op, 2)
|
|
if len(x) != 2:
|
|
op = ":="
|
|
x = line.split(op, 2)
|
|
if len(x) != 2:
|
|
op = "="
|
|
x = line.split(op, 2)
|
|
if len(x) != 2:
|
|
print(" Error: Configuration: ", line, file=sys.stderr)
|
|
return
|
|
key,val = x
|
|
|
|
key = 'conf_' + key.strip().lower()
|
|
if key not in globals():
|
|
print(" Warn: Configuration not found: ", key, file=sys.stderr)
|
|
return
|
|
|
|
if False: pass
|
|
elif op == '=':
|
|
val = val.strip()
|
|
if False: pass
|
|
elif val.lower() in ("false", "no"): val = False
|
|
elif val.lower() in ("true", "yes"): val = True
|
|
elif val == '[]': val = []
|
|
elif val == '{}': val = {}
|
|
elif val.isdigit(): val = int(val)
|
|
|
|
if type(globals()[key]) != type(val):
|
|
print(" Error: Configuration ", key,'bad:',val, file=sys.stderr)
|
|
return
|
|
globals()[key] = val
|
|
elif op == '+=':
|
|
val = val.strip()
|
|
if type(globals()[key]) != type([]):
|
|
print(" Error: Configuration ", key, 'not []', file=sys.stderr)
|
|
return
|
|
globals()[key].append(val)
|
|
elif op == ':=':
|
|
if type(globals()[key]) != type({}):
|
|
print(" Error: Configuration ", key, 'not {}', file=sys.stderr)
|
|
return
|
|
|
|
if '=' not in val:
|
|
print(" Error: Configuration bad :=", file=sys.stderr)
|
|
return
|
|
dkey, dval = val.split('=', 1)
|
|
globals()[key][dkey.strip()] = dval.strip()
|
|
else:
|
|
print(" Error: Configuration ", key,'bad op', file=sys.stderr)
|
|
return
|
|
|
|
|
|
# This is kind of fast and kind of small. No re, and no allocation.
|
|
# Sort as: 0, 00, 000, 01, 011, 1, 11, a01, a1, z01, z1, etc.
|
|
def natcmp(x, y):
|
|
""" Natural sort string comparison.
|
|
https://en.wikipedia.org/wiki/Natural_sort_order
|
|
Aka. vercmp() """
|
|
|
|
def _cmp_xy_mix(): # One is a digit, the other isn't.
|
|
if inum is not None: # 0/1 vs. x/.
|
|
return 1
|
|
if x[i] > y[i]:
|
|
return 1
|
|
else:
|
|
return -1
|
|
|
|
inum = None
|
|
check_zeros = False
|
|
for i in range(min(len(x), len(y))):
|
|
if x[i] in "0123456789" and y[i] not in "0123456789":
|
|
return _cmp_xy_mix()
|
|
if x[i] not in "0123456789" and y[i] in "0123456789":
|
|
return _cmp_xy_mix()
|
|
|
|
if x[i] in "0123456789": # Both are digits...
|
|
if inum is None:
|
|
check_zeros = True
|
|
inum = 0
|
|
|
|
if check_zeros: # Leading zeros... (0 < 00 < 01 < 011 < 1 < 11)
|
|
if x[i] == '0' and y[i] == '0':
|
|
continue
|
|
elif x[i] == '0':
|
|
return -1
|
|
elif y[i] == '0':
|
|
return 1
|
|
else:
|
|
check_zeros = False
|
|
|
|
# If we are already in a number, we only care about the length or
|
|
# the first digit that is different.
|
|
if inum != 0:
|
|
continue
|
|
|
|
if x[i] == y[i]:
|
|
continue
|
|
|
|
# Non-zero first digit, Eg. 7 < 9
|
|
inum = int(x[i]) - int(y[i])
|
|
continue
|
|
|
|
# Both are not digits...
|
|
if inum is not None and inum != 0:
|
|
return inum
|
|
inum = None
|
|
|
|
# Can be equal
|
|
if x[i] > y[i]:
|
|
return 1
|
|
if x[i] < y[i]:
|
|
return -1
|
|
|
|
if len(x) > len(y):
|
|
if inum is not None and inum != 0 and x[i+1] not in "0123456789":
|
|
return inum
|
|
return 1
|
|
if len(x) < len(y):
|
|
if inum is not None and inum != 0 and y[i+1] not in "0123456789":
|
|
return inum
|
|
return -1
|
|
|
|
if inum is None: # Same length, not in a num.
|
|
assert x == y
|
|
return 0 # So the strings are equal.
|
|
|
|
return inum
|
|
|
|
|
|
class NatCmp():
|
|
__slots__ = ['s',]
|
|
def __init__(self, s):
|
|
self.s = s
|
|
|
|
def __str__(self):
|
|
return self.s
|
|
|
|
def __eq__(self, other):
|
|
return self.s == other.s
|
|
|
|
def __gt__(self, other):
|
|
ret = natcmp(self.s, other.s)
|
|
if ret > 0:
|
|
return True
|
|
return False
|
|
|
|
# Given a list of strings, sort them using natcmp()
|
|
def nat_sorted(xs):
|
|
for ret in sorted(NatCmp(x) for x in xs):
|
|
yield ret.s
|
|
|
|
|
|
def _fnmatchi(path, pat):
|
|
""" Simple way to always use case insensitive filename matching. """
|
|
return fnmatch.fnmatch(path.lower(), pat.lower())
|
|
|
|
# Have nice "plain" numbers...
|
|
def _ui_int(num):
|
|
if conf_num_sep_:
|
|
return "{:_}".format(int(num))
|
|
return "{:,}".format(int(num))
|
|
|
|
# See: https://en.wikipedia.org/wiki/ANSI_escape_code#Select_Graphic_Rendition_parameters
|
|
# We merge the colours for 16 values so fg0-fgf bg0-bgf
|
|
ansi = {'bold' : '\033[1m', 'dim' : '\033[2m', 'italic' : '\033[3m',
|
|
'underline' : '\033[4m', 'blink' : '\033[5m', 'reverse' :'\033[7m'}
|
|
ansi_stop = '\033[0m'
|
|
for i in range(7):
|
|
ansi['fg:' + str(i)] = '\033[3' + str(i) + 'm'
|
|
ansi['bg:' + str(i)] = '\033[4' + str(i) + 'm'
|
|
for i, j in ((0, '8'), (1, '9'), (2, 'a'), (3, 'b'),
|
|
(4, 'c'), (5, 'd'), (6, 'e'), (7, 'f')):
|
|
ansi['fg:' + j] = '\033[9' + str(i) + 'm'
|
|
ansi['bg:' + j] = '\033[10' + str(i) + 'm'
|
|
def _ui_t_align(text, align=None, olen=None):
|
|
if align is None or align == 0:
|
|
return text
|
|
if olen is None:
|
|
olen = len(text)
|
|
if abs(align) > olen: # "%*s", align, text
|
|
extra = abs(align) - olen
|
|
if align > 0:
|
|
text = " " * extra + text
|
|
else:
|
|
text = text + " " * extra
|
|
return text
|
|
def _ui_t_ansi(text, codes, align=0):
|
|
olen = len(text)
|
|
text = _ui_t_align(text, align)
|
|
if not conf_ansi_terminal or not codes or olen == 0:
|
|
return text
|
|
|
|
esc = ''
|
|
for c in codes.split(','):
|
|
if c == 'reset':
|
|
esc = ''
|
|
if c not in ansi: # Ignore bad codes
|
|
continue
|
|
esc += ansi[c]
|
|
|
|
if not esc:
|
|
return text
|
|
# Deal with leading/trailing spaces, mainly for underline.
|
|
olen = len(text)
|
|
text = text.lstrip()
|
|
prefix = olen - len(text)
|
|
text = text.rstrip()
|
|
suffix = (olen - prefix) - len(text)
|
|
return "%*s%s%s%s%*s" % (prefix, '', esc, text, ansi_stop, suffix, '')
|
|
|
|
def _ui_t_cmd(text, align=0):
|
|
return _ui_t_ansi(text, conf_term_cmd, align=align)
|
|
def _ui_t_high(text, align=0):
|
|
return _ui_t_ansi(text, conf_term_highlight, align=align)
|
|
def _ui_t_key(text, align=0):
|
|
return _ui_t_ansi(text, conf_term_keyword, align=align)
|
|
def _ui_t_time(text, align=0):
|
|
return _ui_t_ansi(text, conf_term_time, align=align)
|
|
def _ui_t_title(text, align=0):
|
|
return _ui_t_ansi(text, conf_term_title, align=align)
|
|
|
|
def _ui_t_diffstat_hostnum(text, align=0):
|
|
return _ui_t_ansi(text, conf_term_diffstat_hostnum, align=align)
|
|
def _ui_t_diffstat_updates(text, align=0):
|
|
return _ui_t_ansi(text, conf_term_diffstat_updates, align=align)
|
|
def _ui_t_diffstat_added(text, align=0):
|
|
return _ui_t_ansi(text, conf_term_diffstat_added, align=align)
|
|
def _ui_t_diffstat_instl(text, align=0):
|
|
return _ui_t_ansi(text, conf_term_diffstat_instl, align=align)
|
|
def _ui_t_diffstat_boots(text, align=0):
|
|
return _ui_t_ansi(text, conf_term_diffstat_boots, align=align)
|
|
|
|
# Make it easier to spot when hosts aren't getting their data updated.
|
|
def _ui_date(d1, align=None, prev=None):
|
|
if not conf_ui_date:
|
|
return _ui_t_align(d1.date, align)
|
|
if prev is not None and d1.date == prev:
|
|
return _ui_t_align(" \" ", align)
|
|
if False and d1.date == backup_today: # Better, or no?
|
|
return _ui_t_align("today", align)
|
|
if False and d1.date == backup_yesterday:
|
|
return _ui_t_align("yesterday", align)
|
|
# YYYY-MM-DD
|
|
# 1234567890
|
|
if prev is None:
|
|
prev = backups[-1]
|
|
if conf_ansi_terminal and d1.date != prev:
|
|
for i in (9, 8, 7, 5):
|
|
if d1.date[:i] == prev[:i]:
|
|
ndate = d1.date[:i] + _ui_t_high(d1.date[i:])
|
|
return _ui_t_align(ndate, align, len(d1.date))
|
|
return _ui_t_align(d1.date, align)
|
|
|
|
# History files are named <fname>.YYYY-MM-DD
|
|
def _glob_hist_suffix():
|
|
for fn in os.listdir(os.path.dirname(fname)):
|
|
if not fn.startswith(_fname + '.'):
|
|
continue
|
|
fn = fn.removeprefix(_fname + '.')
|
|
if len(fn) != len("YYYY-MM-DD"):
|
|
continue
|
|
if fn[0] != '2': continue # Year
|
|
if fn[1] not in "0123456789": continue
|
|
if fn[2] not in "0123456789": continue
|
|
if fn[3] not in "0123456789": continue
|
|
if fn[4] != '-': continue
|
|
if fn[5] not in "01": continue # Month
|
|
if fn[6] not in "0123456789": continue
|
|
if fn[7] != '-': continue
|
|
if fn[8] not in "0123": continue # Day
|
|
if fn[9] not in "0123456789": continue
|
|
yield fn
|
|
|
|
_fname = "ansible-list-updates-uptime.txt"
|
|
def _pre_cmd__setup():
|
|
global conf_path
|
|
global fname
|
|
global fname_today
|
|
global fname_yesterday
|
|
global backups
|
|
global backup_today
|
|
global backup_yesterday
|
|
global conf_ansi_terminal
|
|
|
|
conf_path = os.path.expanduser(conf_path)
|
|
|
|
if conf_path[0] != '/':
|
|
print(" Warning: Conf path isn't absolute", file=sys.stderr)
|
|
|
|
_suffix_dns_replace.clear()
|
|
for x in conf_suffix_dns_replace:
|
|
_suffix_dns_replace[x] = False
|
|
if conf_ansi_terminal is None:
|
|
conf_ansi_terminal = sys.stdout.isatty()
|
|
fname = conf_path + _fname
|
|
backup_today = time.strftime("%Y-%m-%d", time.gmtime())
|
|
fname_today = fname + '.' + backup_today
|
|
backups = sorted(_glob_hist_suffix())
|
|
|
|
tm_yesterday = int(time.time()) - (60*60*24)
|
|
backup_yesterday = time.strftime("%Y-%m-%d", time.gmtime(tm_yesterday))
|
|
fname_yesterday = fname + '.' + backup_yesterday
|
|
|
|
if len(backups) < 1 or backups[-1] != backup_today:
|
|
fname_today = None
|
|
if len(backups) < 2 or backups[-2] != backup_yesterday:
|
|
if fname_today is None and backups and backups[-1] == backup_yesterday:
|
|
pass # Just missing today
|
|
else:
|
|
fname_yesterday = None
|
|
|
|
|
|
def _pre_cmd__verbose(args):
|
|
if args.verbose <= 0:
|
|
return
|
|
|
|
if args.verbose >= 3:
|
|
globals()['conf_ui_date'] = False
|
|
if args.verbose >= 2:
|
|
globals()['conf_small_osinfo'] = False
|
|
globals()['conf_suffix_dns_replace'] = {}
|
|
globals()['conf_hist_show'] = 0
|
|
globals()['conf_host_end_total_hostnum'] = 0
|
|
globals()['conf_host_skip_eq'] = False
|
|
globals()['conf_info_machine_ids'] = True
|
|
globals()['conf_short_duration'] = False
|
|
globals()['conf_stat_4_hosts'] *= (args.verbose * 2)
|
|
|
|
def _wild_eq(s1, s2):
|
|
""" Compare two strings, but allow '?' to mean anything. """
|
|
if s1 == '?' or s2 == '?':
|
|
return True
|
|
return s1 == s2
|
|
|
|
|
|
_max_len_osnm = 0 # osname_small
|
|
_max_len_osvr = 0 # osvers ... upto the first '.'
|
|
class Host():
|
|
""" Class for holding the Host data from a line in the files. """
|
|
|
|
__slots__ = ['name', 'rpms', 'uptime', 'date', 'osname', 'osvers',
|
|
'osname_small', 'machine_id', 'boot_id']
|
|
|
|
def __init__ (self, data):
|
|
global _max_len_osnm
|
|
global _max_len_osvr
|
|
|
|
self.name = data['name']
|
|
self.rpms = data['rpms']
|
|
self.uptime = data['uptime']
|
|
self.date = data['date']
|
|
|
|
self.osname = data['osname']
|
|
self.osvers = data['osvers']
|
|
|
|
self.machine_id = data['machine_id']
|
|
self.boot_id = data['boot_id']
|
|
|
|
if False: pass
|
|
elif self.osname == 'CentOS':
|
|
osname_small = 'EL'
|
|
elif self.osname == 'RedHat':
|
|
osname_small = 'EL'
|
|
elif self.osname == 'Fedora':
|
|
osname_small = 'F'
|
|
else:
|
|
osname_small = self.osname[:3]
|
|
self.osname_small = osname_small
|
|
|
|
_max_len_osnm = max(len(osname_small), _max_len_osnm)
|
|
vers = self.osvers
|
|
off = vers.find('.')
|
|
if off != -1:
|
|
vers = vers[:off]
|
|
_max_len_osvr = max(len(vers), _max_len_osvr)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def __eq__(self, other):
|
|
if self.name != other.name:
|
|
return False
|
|
if self.rpms != other.rpms:
|
|
return False
|
|
if not _wild_eq(self.osname, other.osname):
|
|
return False
|
|
if not _wild_eq(self.osvers, other.osvers):
|
|
return False
|
|
if not _wild_eq(self.machine_id, other.machine_id):
|
|
return False
|
|
return True
|
|
|
|
def __gt__(self, other):
|
|
ret = natcmp(self.name, other.name)
|
|
if ret > 0:
|
|
return True
|
|
if ret < 0:
|
|
return False
|
|
|
|
if self.rpms > other.rpms:
|
|
return True
|
|
if self.rpms != other.rpms:
|
|
return False
|
|
|
|
if self.osname > other.osname:
|
|
return True
|
|
if not _wild_eq(self.osname, other.osname):
|
|
return False
|
|
|
|
if self.osvers > other.osvers:
|
|
return True
|
|
return False
|
|
|
|
# Pretend to be a dict...
|
|
def __getitem__(self, key):
|
|
if key not in self.__slots__:
|
|
raise KeyError()
|
|
return getattr(self, key)
|
|
|
|
@property
|
|
def osinfo(self):
|
|
return "%s/%s" % (self.osname, self.osvers)
|
|
|
|
@property
|
|
def osinfo_small(self):
|
|
if conf_align_osinfo_small:
|
|
vers = self.osvers
|
|
rest = ''
|
|
off = vers.find('.')
|
|
if off != -1:
|
|
rest = vers[off:]
|
|
vers = vers[:off]
|
|
return "%*s/%*s%s" % (_max_len_osnm, self.osname_small,
|
|
_max_len_osvr, vers, rest)
|
|
return "%s/%s" % (self.osname_small, self.osvers)
|
|
|
|
@property
|
|
def date_tm(self):
|
|
return time.mktime(time.strptime(self.date, "%Y-%m-%d"))
|
|
|
|
|
|
_tm_d = {'d' : 60*60*24, 'h' : 60*60, 'm' : 60, 's' : 1,
|
|
'w' : 60*60*24*7,
|
|
'q' : 60*60*24*7*13}
|
|
def parse_duration(seconds):
|
|
if seconds is None:
|
|
return None
|
|
if seconds.isdigit():
|
|
return int(seconds)
|
|
|
|
ret = 0
|
|
for mark in ('w', 'd', 'h', 'm', 's'):
|
|
pos = seconds.find(mark)
|
|
if pos == -1:
|
|
continue
|
|
val = seconds[:pos]
|
|
seconds = seconds[pos+1:]
|
|
if not val.isdigit():
|
|
# dbg("!isdigit", val)
|
|
return None
|
|
ret += _tm_d[mark]*int(val)
|
|
if seconds.isdigit():
|
|
ret += int(seconds)
|
|
elif seconds != '':
|
|
# dbg("!empty", seconds)
|
|
return None
|
|
|
|
return ret
|
|
|
|
|
|
def _add_dur(dur, ret, nummod, suffix, static=False):
|
|
mod = dur % nummod
|
|
dur = dur // nummod
|
|
if mod > 0 or (static and dur > 0):
|
|
ret.append(suffix)
|
|
if static and dur > 0:
|
|
ret.append("%0*d" % (len(str(nummod)), mod))
|
|
else:
|
|
ret.append(str(mod))
|
|
return dur
|
|
|
|
def format_duration(seconds, short=False, static=False):
|
|
if seconds is None:
|
|
seconds = 0
|
|
dur = int(seconds)
|
|
|
|
ret = []
|
|
dur = _add_dur(dur, ret, 60, "s", static=static)
|
|
dur = _add_dur(dur, ret, 60, "m", static=static)
|
|
if short:
|
|
if dur == 0 and not static:
|
|
return '<1h'
|
|
if dur == 0:
|
|
return '<01h'
|
|
ret = []
|
|
dur = _add_dur(dur, ret, 24, "h", static=static)
|
|
dur = _add_dur(dur, ret, 7, "d", static=static)
|
|
if dur > 0:
|
|
ret.append("w")
|
|
ret.append(str(dur))
|
|
return "".join(reversed(ret))
|
|
|
|
# Duration in UI for lists/etc.
|
|
def _ui_dur(dur, short=None):
|
|
if short is None:
|
|
short = conf_short_duration
|
|
return format_duration(dur, short=short, static=True)
|
|
|
|
def _main_file_recent():
|
|
f1 = os.stat(fname)
|
|
|
|
if (int(time.time()) - f1.st_mtime) > (60*60*24):
|
|
return False
|
|
return True
|
|
|
|
def _files_identical(fn1, fn2):
|
|
f1 = os.stat(fn1)
|
|
f2 = os.stat(fn2)
|
|
if f1.st_size != f2.st_size:
|
|
return False
|
|
if (f1.st_mtime - f2.st_mtime) > 64: # seconds, just a copy
|
|
return False
|
|
return True
|
|
|
|
def _backup_yesterday_identical():
|
|
if fname_yesterday is None:
|
|
return False
|
|
|
|
return _files_identical(fname, fname_yesterday)
|
|
|
|
def _backup_today_identical():
|
|
if fname_today is None:
|
|
return False
|
|
|
|
return _files_identical(fname, fname_today)
|
|
|
|
_max_len_osnm = 0 # osname_small
|
|
_max_len_osvr = 0 # osvers ... upto the first '.'
|
|
def line2data(line):
|
|
global _max_len_osnm
|
|
global _max_len_osvr
|
|
|
|
name, rpms, uptime, date = line.split(' ', 3)
|
|
osname = "?"
|
|
osvers = "?"
|
|
machine_id = "?"
|
|
boot_id = "?"
|
|
if ' ' in date:
|
|
date, osname, osvers = date.split(' ', 2)
|
|
if ' ' in osvers:
|
|
osvers, machine_id, boot_id = osvers.split(' ', 2)
|
|
|
|
rpms = int(rpms)
|
|
uptime = int(uptime)
|
|
|
|
return Host(locals())
|
|
|
|
def lines2datas(lines):
|
|
return (line2data(line) for line in lines)
|
|
|
|
# Filter datas using name as a filename wildcard match.
|
|
def filter_name_datas(datas, names):
|
|
if not names: # Allow everything...
|
|
for data in datas:
|
|
yield data
|
|
return
|
|
|
|
for data in datas:
|
|
for name in names:
|
|
if _fnmatchi(data.name, name):
|
|
break
|
|
else:
|
|
continue
|
|
yield data
|
|
|
|
# Filter datas using osname/vers/info as a filename wildcard match.
|
|
def filter_osname_datas(datas, names):
|
|
if not names: # Allow everything...
|
|
for data in datas:
|
|
yield data
|
|
return
|
|
|
|
for data in datas:
|
|
for name in names:
|
|
if _fnmatchi(data.osinfo, name):
|
|
break
|
|
if _fnmatchi(data.osinfo_small, name):
|
|
break
|
|
if _fnmatchi(data.osname, name):
|
|
break
|
|
if _fnmatchi(data.osname_small, name):
|
|
break
|
|
if _fnmatchi(data.osvers, name):
|
|
break
|
|
off = data.osvers.find('.')
|
|
if off != -1:
|
|
vers = data.osvers[:off]
|
|
if _fnmatchi(vers, name):
|
|
break
|
|
|
|
else:
|
|
continue
|
|
yield data
|
|
|
|
# Filter datas using the date the data is gathered
|
|
def filter_age_min_datas(datas, age):
|
|
now = time.time()
|
|
for data in datas:
|
|
if age > 0 and (now - data.date_tm) < age:
|
|
continue
|
|
yield data
|
|
|
|
# Filter datas using uptime as a minium.
|
|
def filter_uptime_min_datas(datas, uptime):
|
|
for data in datas:
|
|
if uptime > 0 and data.uptime < uptime:
|
|
continue
|
|
yield data
|
|
|
|
# Filter datas using uptime as a maximum.
|
|
def filter_uptime_max_datas(datas, uptime):
|
|
for data in datas:
|
|
if uptime > 0 and data.uptime > uptime:
|
|
continue
|
|
yield data
|
|
|
|
# Sub. suffix of DNS names for UI
|
|
def _ui_name(name):
|
|
for suffix in conf_suffix_dns_replace:
|
|
if name.endswith(suffix):
|
|
_suffix_dns_replace[suffix] = True
|
|
return name[:-len(suffix)] + conf_suffix_dns_replace[suffix]
|
|
return name
|
|
|
|
def _ui_osinfo(data):
|
|
if conf_small_osinfo:
|
|
return data.osinfo_small
|
|
return data.osinfo
|
|
|
|
# Reset the usage after _max_update()
|
|
def _reset_ui_name():
|
|
for suffix in sorted(_suffix_dns_replace):
|
|
_suffix_dns_replace[suffix] = False
|
|
|
|
# Explain if we used any suffix subs.
|
|
def _explain_ui_name():
|
|
done = False
|
|
pre = "* NOTE:"
|
|
for suffix in sorted(_suffix_dns_replace):
|
|
if _suffix_dns_replace[suffix]:
|
|
print("%s %12s = %s" % (pre,conf_suffix_dns_replace[suffix],suffix))
|
|
pre = " :"
|
|
done = True
|
|
if done:
|
|
print(" : Use -vv to show full names.")
|
|
|
|
def fname2lines(fname):
|
|
return [x.strip() for x in open(fname).readlines()]
|
|
|
|
def bfname2lines(b):
|
|
return fname2lines(fname + '.' + b)
|
|
|
|
def _maybe_dynamic_uptime(data):
|
|
""" Only call this for the main file data. """
|
|
if not conf_dynamic_main_uptime:
|
|
return data
|
|
|
|
mtime = os.path.getmtime(fname)
|
|
since = int(time.time()) - int(mtime)
|
|
data = list(sorted(data))
|
|
for d1 in data:
|
|
d1.uptime += since
|
|
return data
|
|
|
|
def fname_datas():
|
|
return _maybe_dynamic_uptime(lines2datas(fname2lines(fname)))
|
|
|
|
|
|
# We don't want to diff against the latest backup, if that backup is identical
|
|
def _diff_latest():
|
|
ret = backups[-1] # Most recent
|
|
ident = None
|
|
if fname_today is not None:
|
|
ident = _backup_today_identical()
|
|
elif fname_yesterday is not None:
|
|
ident = _backup_yesterday_identical()
|
|
if ident is not None and ident:
|
|
# Eg. if you just do one update a day, you want to cmp vs.
|
|
# the previous day, not today.
|
|
ret = backups[-2]
|
|
return ret
|
|
|
|
# Alters the arguments, so we know wtf the filename is.
|
|
def fname1(args=[]):
|
|
_dbg_fname1 = False
|
|
if _dbg_fname1: print("JDBG: fname1:", args)
|
|
if args and args[0] not in backups:
|
|
if args[0] in ("diff-latest", "diff-newest"):
|
|
args[0] = _diff_latest()
|
|
|
|
if args[0] in ("latest", "newest"):
|
|
args[0] = backups[-1] # Most recent
|
|
if args[0] == "oldest":
|
|
args[0] = backups[0]
|
|
if args[0] == "today":
|
|
if fname_today is not None:
|
|
args[0] = backup_today
|
|
else:
|
|
args[0] = "main"
|
|
if args[0] == "yesterday" and fname_yesterday is not None:
|
|
if fname_yesterday is not None:
|
|
args[0] = backup_yesterday
|
|
else:
|
|
args[0] = "main"
|
|
if _dbg_fname1: print("JDBG: fname1.2:", args)
|
|
if args and args[0] != "main":
|
|
return lines2datas(bfname2lines(args[0]))
|
|
return fname_datas()
|
|
|
|
# Has the host been rebooted between these two points.
|
|
def host_rebooted(d1, d2):
|
|
# This is easy mode, just compare boot ids
|
|
if d1.boot_id != '?' and d2.boot_id != '?':
|
|
return d1.boot_id != d2.boot_id
|
|
|
|
# Now we try to work it out from uptime...
|
|
if d1.date == d2.date and d1.uptime > d2.uptime:
|
|
return True
|
|
# However, we can be looking at old history
|
|
tm1 = d1.date_tm
|
|
tm2 = d2.date_tm
|
|
if tm1 > tm2: # Looking backwards in time...
|
|
return False
|
|
d1up = d1.uptime
|
|
tmdiff = tm2 - tm1
|
|
if tmdiff > conf_tmdiff_fudge:
|
|
d1up += tmdiff - conf_tmdiff_fudge
|
|
return d1up > d2.uptime
|
|
|
|
_max_len_name = 0
|
|
_max_len_rpms = 0 # Number of rpm updates via. _ui_int().
|
|
_max_len_upts = 0 # Uptime duration with short=True
|
|
_max_len_date = 0 # 2025-08-04 = 4+1+2+1+2
|
|
_max_terminal_width = shutil.get_terminal_size().columns
|
|
if _max_terminal_width < 20:
|
|
_max_terminal_width = 80
|
|
_max_terminal_width -= 14
|
|
def _max_update(datas):
|
|
for data in datas:
|
|
_max_update_data(data)
|
|
|
|
def _max_update_data(data):
|
|
global _max_len_name
|
|
global _max_len_rpms
|
|
global _max_len_upts
|
|
global _max_len_date
|
|
|
|
name = _ui_name(data.name)
|
|
if len(name) > _max_len_name:
|
|
_max_len_name = len(name)
|
|
|
|
rpms = _ui_int(data.rpms)
|
|
if len(rpms) > _max_len_rpms:
|
|
_max_len_rpms = len(rpms)
|
|
|
|
upts_len = len(_ui_dur(data.uptime))
|
|
if upts_len > _max_len_upts:
|
|
_max_len_upts = upts_len
|
|
|
|
if len(data.date) > _max_len_date:
|
|
_max_len_date = len(data.date)
|
|
|
|
def _max_update_correct(prefix):
|
|
global _max_len_name
|
|
global _max_len_rpms
|
|
global _max_len_upts
|
|
global _max_len_date
|
|
mw = _max_terminal_width - len(prefix)
|
|
if _max_len_name + _max_len_rpms + _max_len_upts + _max_len_date < (mw-8):
|
|
_max_len_name += 1
|
|
_max_len_rpms += 1
|
|
_max_len_upts += 1
|
|
_max_len_date += 1
|
|
|
|
while _max_len_name + _max_len_rpms + _max_len_upts + _max_len_date >= mw:
|
|
_max_len_name -= 1
|
|
|
|
def _wild_info_eq(d1, d2):
|
|
if not _wild_eq(d1.osname, d2.osname):
|
|
return False
|
|
if not _wild_eq(d1.osvers, d2.osvers):
|
|
return False
|
|
return True
|
|
|
|
# Return stats for updates added/deleted between two data sets.
|
|
def _diffstats(data1, data2):
|
|
uadd, udel, boot = 0, 0, 0
|
|
|
|
data1 = list(sorted(data1))
|
|
data2 = list(sorted(data2))
|
|
while len(data1) > 0 or len(data2) > 0:
|
|
if len(data1) <= 0:
|
|
d2 = data2.pop(0)
|
|
uadd += d2.rpms
|
|
boot += 1
|
|
continue
|
|
if len(data2) <= 0:
|
|
d1 = data1.pop(0)
|
|
udel -= d1.rpms
|
|
continue
|
|
|
|
d1 = data1[0]
|
|
d2 = data2[0]
|
|
|
|
if d1.name < d2.name:
|
|
udel -= d1.rpms
|
|
data1.pop(0)
|
|
continue
|
|
|
|
if d1.name > d2.name:
|
|
uadd += d2.rpms
|
|
data2.pop(0)
|
|
boot += 1
|
|
continue
|
|
|
|
if d1 == d2:
|
|
if host_rebooted(d1, d2):
|
|
boot += 1
|
|
data1.pop(0)
|
|
data2.pop(0)
|
|
continue
|
|
|
|
if (not _wild_eq(d1.machine_id, d2.machine_id) or
|
|
not _wild_info_eq(d1, d2)):
|
|
boot += 1
|
|
udel -= d1.rpms
|
|
uadd += d2.rpms
|
|
data1.pop(0)
|
|
data2.pop(0)
|
|
continue
|
|
|
|
# Now name is eq and osinfo is eq
|
|
if host_rebooted(d1, d2):
|
|
boot += 1
|
|
# So either new updates arrived, or we installed some and they went
|
|
# down ... alas. we can't tell if both happened.
|
|
if d1.rpms > d2.rpms:
|
|
udel -= d1.rpms - d2.rpms
|
|
if d1.rpms < d2.rpms:
|
|
uadd += d2.rpms - d1.rpms
|
|
data1.pop(0)
|
|
data2.pop(0)
|
|
|
|
# diffstat returns...
|
|
return uadd, udel, boot
|
|
|
|
def _ui_diffstats(data1, data2):
|
|
cmpds = _diffstats(data1.copy(), data2.copy())
|
|
return _ui_int(cmpds[0]), _ui_int(cmpds[1]), _ui_int(cmpds[2])
|
|
|
|
def _print_diffstats(hostnum, updates, cmpds):
|
|
hostnum = _ui_t_diffstat_hostnum(str(hostnum))
|
|
updates = _ui_t_diffstat_updates(str(updates))
|
|
added = _ui_t_diffstat_added(cmpds[0])
|
|
instl = _ui_t_diffstat_instl(cmpds[1])
|
|
boots = _ui_t_diffstat_boots(cmpds[2])
|
|
print('hosts=%s updates=%s (a=%s i=%s) boots=%s' % (hostnum, updates,
|
|
added, instl, boots))
|
|
|
|
|
|
|
|
# This is the real __main__ start ...
|
|
def _usage(short=False):
|
|
prog = "updates+uptime"
|
|
if sys.argv:
|
|
prog = os.path.basename(sys.argv[0])
|
|
print("""\
|
|
Usage: %s <cmd>
|
|
Optional arguments:
|
|
--help, -h Show this help message and exit.
|
|
--verbose, -v Increase verbosity.
|
|
--conf CONF Specify configuration.
|
|
--db-dir DB_DIR Change the path to the files.
|
|
--ansi ANSI Use ansi terminal codes.
|
|
Cmds:
|
|
""" % (prog,), end='')
|
|
if short:
|
|
print("""\
|
|
created [duration] [host*]...
|
|
diff/-u [backup1] [backup2]
|
|
help
|
|
history
|
|
history-keep [days]
|
|
hosts/-u [host*] [host*]...
|
|
info [host*] [backup] [backup]...
|
|
list [host*] [backup] [host*]...
|
|
list [host*]...
|
|
old-list duration
|
|
oslist [os*] [backup] [os*]...
|
|
oslist [os*]...
|
|
stats [host*]...
|
|
update
|
|
update-host host
|
|
uptime/-min/-max duration [backup]
|
|
""", end='')
|
|
else:
|
|
# Also see: _cmd_help() below...
|
|
print("""\
|
|
diff [backup1] [backup2]
|
|
= See the difference between the current state and backups.
|
|
diff-u [backup1] [backup2]
|
|
= Shows before/after instead of modified (like diff -u).
|
|
|
|
history
|
|
--osnames os*
|
|
= Show summary of current data, and how it changed over time.
|
|
history-keep [days]
|
|
= Cleanup old history.
|
|
|
|
hosts/-u [host*] [host*]...
|
|
--osnames os*
|
|
= See the history of a host(s).
|
|
info [host*] [backup] [backup]...
|
|
= See the current state, in long form, can be filtered by name.
|
|
list [host*]...
|
|
--history backup
|
|
--osnames os*
|
|
= See the current state, can be filtered by name.
|
|
old-list duration
|
|
= See the current state of hosts with data older than duration.
|
|
oslist [os*]...
|
|
--history backup
|
|
= See the current state, can be filtered by OS.
|
|
|
|
stats [host*]...
|
|
--history backup
|
|
= Show general stats.
|
|
|
|
update
|
|
= Run update-fast, or update-daily if no daily backup.
|
|
update-fast
|
|
update-host host
|
|
= Create/update the main file, for the specified host(s).
|
|
update-daily
|
|
= Run update-fast and force do a backup for today.
|
|
update-daily-refresh
|
|
= Run update-daily with an empty main file.
|
|
|
|
uptime-min duration [backup]
|
|
= See the current state, can be filtered for uptime >= duration.
|
|
uptime-max duration [backup]
|
|
= See the current state, can be filtered for uptime <= duration.
|
|
""", end='')
|
|
|
|
|
|
def _cmd_history_keep(args):
|
|
keep = args.keep
|
|
while keep < len(backups):
|
|
# We just keep the newest N
|
|
b = backups.pop(0)
|
|
print("Removing:", b)
|
|
fn = fname + '.' + b
|
|
os.unlink(fn)
|
|
|
|
def inventory_hosts():
|
|
# The "correct" way to do this is something like:
|
|
# ansible-inventory --list | jq -r '._meta.hostvars | keys[]'
|
|
# ...but that is _much_ slower, as it's loading a lot of data/facts which
|
|
# we ignore.
|
|
cmds = ["ansible", "all", "--list-host"]
|
|
p = subprocess.Popen(cmds, text=True, stdout=subprocess.PIPE)
|
|
header = p.stdout.readline()
|
|
if not header.strip().startswith("hosts ("):
|
|
return set()
|
|
|
|
ret = set()
|
|
for line in p.stdout:
|
|
ret.add(line.strip())
|
|
return ret
|
|
|
|
def remove_old_hosts():
|
|
if not os.path.exists(fname):
|
|
return
|
|
|
|
inv = inventory_hosts()
|
|
if not inv: # If we can't get inventory, don't delete everything.
|
|
return
|
|
|
|
odata = fname2lines(fname)
|
|
fo = open(fname + ".rm_old_hosts.tmp", "w")
|
|
for line in odata:
|
|
host = line.split()[0]
|
|
if host not in inv:
|
|
print("Removing host:", host)
|
|
continue
|
|
fo.write(line)
|
|
fo.write("\n")
|
|
fo.close()
|
|
os.rename(fname + ".rm_old_hosts.tmp", fname)
|
|
|
|
def _cmd_remove_old_hosts(args):
|
|
remove_old_hosts()
|
|
|
|
def _cmd_update(args):
|
|
cmd = args.cmd
|
|
# First remove machines that aren't in inventory anymore, as the playbook
|
|
# only adds/updates them.
|
|
if cmd != "update-daily-refresh": # Just wastes time if refresh.
|
|
remove_old_hosts()
|
|
|
|
# Now update whatever is left.
|
|
if cmd == "update":
|
|
cmd = "update-flush"
|
|
if not os.path.exists(fname):
|
|
cmd = "update-daily" # This does the sorting etc.
|
|
elif fname_today is None:
|
|
cmd = "update-daily"
|
|
elif False: # -fast and -flush are the same now, so who cares.
|
|
mtime = os.path.getmtime(fname)
|
|
if (int(time.time()) - mtime) > conf_dur_flush_cache:
|
|
cmd = "update-fast"
|
|
|
|
if cmd == "update-host":
|
|
os.chdir("/srv/web/infra/ansible/playbooks")
|
|
os.system("ansible-playbook generate-updates-uptimes-per-host-file.yml -t updates --limit '" + args.host + "'")
|
|
if cmd == "update-flush": # Get the latest uptime.
|
|
# No need to flush caches now, the new playbook should DTRT.
|
|
os.chdir("/srv/web/infra/ansible/playbooks")
|
|
os.system("ansible-playbook generate-updates-uptimes-per-host-file.yml -t updates") # --flush-cache")
|
|
if cmd == "update-fast": # Same as -flush now.
|
|
os.chdir("/srv/web/infra/ansible/playbooks")
|
|
os.system("ansible-playbook generate-updates-uptimes-per-host-file.yml -t updates")
|
|
if cmd == "update-daily-refresh": # Also recreate the main file.
|
|
if os.path.exists(fname):
|
|
os.unlink(fname)
|
|
cmd = "update-daily"
|
|
if cmd == "update-daily": # Also create backup file.
|
|
os.chdir("/srv/web/infra/ansible/playbooks")
|
|
os.system("ansible-playbook generate-updates-uptimes-per-host-file.yml")
|
|
|
|
def _pre_cmd__check_paths():
|
|
# Below here are the query commands, stuff needs to exist at this point.
|
|
if not os.path.exists(fname):
|
|
print(" Error: No main file. Run update sub-command", file=sys.stderr)
|
|
sys.exit(4)
|
|
if not _main_file_recent():
|
|
print(" Warning: Main file is old. Run update sub-command", file=sys.stderr)
|
|
if fname_today is None:
|
|
print(" Warning: History for today does not exist!", file=sys.stderr)
|
|
if fname_yesterday is None:
|
|
print(" Warning: History for yesterday does not exist!", file=sys.stderr)
|
|
|
|
def _backup_suffix(backup):
|
|
suffix = ''
|
|
if backup == backup_today:
|
|
if _backup_today_identical():
|
|
suffix = ' (' + _ui_t_time("today") + ', is eq)'
|
|
else:
|
|
suffix = ' (today)'
|
|
if backup == backup_yesterday:
|
|
if _backup_yesterday_identical():
|
|
suffix = ' (' + _ui_t_time("yesterday") + ', is eq)'
|
|
else:
|
|
suffix = ' (yesterday)'
|
|
return suffix
|
|
|
|
def _hist_lengths(hosts=None):
|
|
# We _could_ open+read+etc each file, just to find out the max updates for
|
|
# all hist ... but len("Updates")+2=9 which means 9,999,999 updates)
|
|
hl = len("Hosts")
|
|
ul = len("Updates")
|
|
rl = len("Boots")
|
|
if conf_fast_width_history:
|
|
ul += 2
|
|
else:
|
|
# Whatever, it's less memory than holding all history at once if you want
|
|
# to enable it..
|
|
for backup in reversed(backups):
|
|
data = list(sorted(lines2datas(bfname2lines(backup))))
|
|
data = list(filter_name_datas(data, hosts))
|
|
updates = _ui_int(sum(d.rpms for d in data))
|
|
hl = max(hl, len(_ui_int(len(data))))
|
|
ul = max(ul, len(updates))
|
|
return hl, ul, rl
|
|
|
|
def _cmd_history(args):
|
|
global backups
|
|
|
|
last_name = "main"
|
|
last_data = list(sorted(fname_datas()))
|
|
last_data = list(filter_name_datas(last_data, args.hosts))
|
|
last_data = list(filter_osname_datas(last_data, args.osnames))
|
|
last_suff = ""
|
|
|
|
# Do this early, so if "not conf_fast_width_history" then we don't do
|
|
# things we don't need to do.
|
|
if conf_hist_show > 0 and conf_hist_show < len(backups):
|
|
backups = backups[-conf_hist_show:]
|
|
|
|
hl, ul, rl = _hist_lengths()
|
|
|
|
print('', _ui_t_title("Day", 10), _ui_t_title("Hosts", hl),
|
|
_ui_t_title("Updates", ul),
|
|
'', _ui_t_title("Added", ul), _ui_t_title("Inst.", ul+1), '',
|
|
_ui_t_title("Boots", rl))
|
|
|
|
for backup in reversed(backups):
|
|
data = list(sorted(lines2datas(bfname2lines(backup))))
|
|
data = list(filter_name_datas(data, args.hosts))
|
|
data = list(filter_osname_datas(data, args.osnames))
|
|
updates = sum(d.rpms for d in last_data)
|
|
less_updates = False
|
|
if updates < sum(d.rpms for d in data):
|
|
less_updates = True
|
|
updates = _ui_int(updates)
|
|
ul = max(ul, len(updates))
|
|
updates = "%*s" % (ul, updates)
|
|
if less_updates:
|
|
updates = _ui_t_high(updates)
|
|
|
|
cmpds = _ui_diffstats(data, last_data)
|
|
|
|
if len(last_data) != len(data):
|
|
nhosts = _ui_t_high("%*s" % (hl, _ui_int(len(last_data))))
|
|
else:
|
|
nhosts = "%*s" % (hl, _ui_int(len(last_data)))
|
|
|
|
print(' %10s %s %s, %*s %*s, %*s %s' % (last_name,
|
|
nhosts, updates,
|
|
ul, cmpds[0], ul+1, cmpds[1], rl, cmpds[2], last_suff))
|
|
last_name = backup
|
|
last_data = data
|
|
last_suff = _backup_suffix(backup)
|
|
updates = _ui_int(sum(d.rpms for d in last_data))
|
|
print(' %10s %*s %*s %s' % (last_name, hl, _ui_int(len(last_data)),
|
|
ul, updates, last_suff))
|
|
|
|
def _cli_match_host(args, data):
|
|
if args.hosts:
|
|
hosts = args.hosts[:]
|
|
print("Matching:", ", ".join(hosts))
|
|
data = filter_name_datas(data, hosts)
|
|
data = list(data)
|
|
if not data:
|
|
print("Not host(s) matched:", ", ".join(hosts))
|
|
sys.exit(2)
|
|
return data
|
|
|
|
def _cmd_stats(args):
|
|
global conf_suffix_dns_replace
|
|
global _max_len_name
|
|
global _max_len_rpms
|
|
global _max_len_upts
|
|
global _max_len_date
|
|
|
|
data = fname1([args.hist])
|
|
data = list(_cli_match_host(args, data))
|
|
# Basically we have hosts/updates/uptime and we want 4 tiers of data:
|
|
# 1. All. 2. OS name (Eg. Fedora). 3. OS name+version (Eg. Fedora 42).
|
|
# 4. For updates/uptime a "few" hosts with the biggest numbers.
|
|
osdata = {'hosts' : {}, 'updates' : {}, 'uptimes' : {}, 'vers' : {}}
|
|
updates = 0 # total updates
|
|
most = [] # Tier 4, for updates
|
|
awake = 0 # total uptime
|
|
awakest = [] # Tier 4, for uptime
|
|
|
|
conf_suffix_dns_replace = {} # Turn off shortened names for stats...
|
|
for d2 in data:
|
|
# Tidy UI for OS names with only one version...
|
|
if d2.osname not in osdata['vers']:
|
|
osdata['vers'][d2.osname] = set()
|
|
osdata['vers'][d2.osname].add(d2.osinfo)
|
|
|
|
# Tier 2/3 hosts...
|
|
if d2.osname not in osdata['hosts']:
|
|
osdata['hosts'][d2.osname] = 0
|
|
osdata['hosts'][d2.osname] += 1
|
|
|
|
if d2.osinfo not in osdata['hosts']:
|
|
osdata['hosts'][d2.osinfo] = 0
|
|
osdata['hosts'][d2.osinfo] += 1
|
|
|
|
updates += d2.rpms
|
|
# Tier 2/3 updates...
|
|
if d2.osname not in osdata['updates']:
|
|
osdata['updates'][d2.osname] = 0
|
|
osdata['updates'][d2.osname] += d2.rpms
|
|
|
|
if d2.osinfo not in osdata['updates']:
|
|
osdata['updates'][d2.osinfo] = 0
|
|
osdata['updates'][d2.osinfo] += d2.rpms
|
|
# Tier 4 updates...
|
|
most.append((d2.rpms, d2.uptime, d2))
|
|
most.sort()
|
|
while len(most) > conf_stat_4_hosts:
|
|
most.pop(0)
|
|
|
|
awake += d2.uptime
|
|
# Tier 2/3 uptimes...
|
|
if d2.osname not in osdata['uptimes']:
|
|
osdata['uptimes'][d2.osname] = 0
|
|
osdata['uptimes'][d2.osname] += d2.uptime
|
|
|
|
if d2.osinfo not in osdata['uptimes']:
|
|
osdata['uptimes'][d2.osinfo] = 0
|
|
osdata['uptimes'][d2.osinfo] += d2.uptime
|
|
# Tier 4 uptimes...
|
|
awakest.append((d2.uptime, d2.rpms, d2))
|
|
awakest.sort()
|
|
while len(awakest) > conf_stat_4_hosts:
|
|
awakest.pop(0)
|
|
|
|
# Print "stats"
|
|
# _max_update(data)
|
|
# Do this by hand...
|
|
_max_len_name = max((len(d.name) for d in data))
|
|
_max_len_rpms = max(len("Updates/h"), len(_ui_int(updates)))
|
|
_max_len_upts = max(len("Uptime/h"), len(_ui_dur(awake)))
|
|
_max_len_date = 0
|
|
_max_update_correct(' ')
|
|
print(_ui_t_title("OS", -16), '', _ui_t_title("Hosts", 6),
|
|
_ui_t_title("Updates", _max_len_rpms),
|
|
_ui_t_title("Uptime", _max_len_upts),
|
|
_ui_t_title("Updates/h", _max_len_rpms),
|
|
_ui_t_title("Uptime/h", _max_len_upts))
|
|
|
|
print("-" * (16+2+6+2*(1+_max_len_rpms+1+_max_len_upts)))
|
|
nhosts = len(data)
|
|
print("%-16s: %6s %*s %*s %*s %*s" % ("All", _ui_int(len(data)),
|
|
_max_len_rpms, _ui_int(updates), _max_len_upts, _ui_dur(awake),
|
|
_max_len_rpms, _ui_int(updates / nhosts),
|
|
_max_len_upts, _ui_dur(awake / nhosts)))
|
|
subprefix = ''
|
|
subplen = 14
|
|
max_nhosts_lvl_1 = 0
|
|
max_update_lvl_1 = 0
|
|
max_uptime_lvl_1 = 0
|
|
max_nhosts_lvl_2 = 0
|
|
max_update_lvl_2 = 0
|
|
max_uptime_lvl_2 = 0
|
|
for osi in nat_sorted(osdata['hosts']):
|
|
if '/' not in osi:
|
|
max_nhosts_lvl_1 = max(max_nhosts_lvl_1, osdata['hosts'][osi])
|
|
supd = osdata['updates'][osi] / osdata['hosts'][osi]
|
|
max_update_lvl_1 = max(max_update_lvl_1, supd)
|
|
supt = osdata['uptimes'][osi] / osdata['hosts'][osi]
|
|
max_uptime_lvl_1 = max(max_uptime_lvl_1, supt)
|
|
|
|
def _ui_up_d_t(osi, lvl):
|
|
if lvl == 1:
|
|
mhosts = max_nhosts_lvl_1
|
|
mupd = max_update_lvl_1
|
|
mupt = max_uptime_lvl_1
|
|
uiosi = "%-14s" % (osi,)
|
|
else:
|
|
mhosts = max_nhosts_lvl_2
|
|
mupd = max_update_lvl_2
|
|
mupt = max_uptime_lvl_2
|
|
uiosi = "%-*s" % (subplen, osi)
|
|
nhosts = osdata['hosts'][osi]
|
|
uinhosts = "%6s" % (_ui_int(nhosts),)
|
|
if nhosts >= mhosts:
|
|
uinhosts = _ui_t_high(uinhosts)
|
|
uiupdates = "%*s" % (_max_len_rpms, _ui_int(osdata['updates'][osi]))
|
|
uiuptimes = "%*s" % (_max_len_upts, _ui_dur(osdata['uptimes'][osi]))
|
|
num = osdata['updates'][osi] / nhosts
|
|
uinpdates = "%*s" % (_max_len_rpms, _ui_int(num))
|
|
bigger = False
|
|
suf = ''
|
|
if num >= mupd:
|
|
bigger = True
|
|
suf += ' *'
|
|
# suf += ' Up'
|
|
# uiupdates = _ui_t_high(uiupdates)
|
|
uinpdates = _ui_t_high(uinpdates)
|
|
num = osdata['uptimes'][osi] / nhosts
|
|
uinptimes = "%*s" % (_max_len_upts, _ui_dur(num))
|
|
if num >= mupt:
|
|
bigger = True
|
|
# suf += ' Tm'
|
|
# uiuptimes = _ui_t_high(uiuptimes)
|
|
uinptimes = _ui_t_high(uinptimes)
|
|
if bigger:
|
|
uiosi = _ui_t_high(uiosi)
|
|
if lvl == 1:
|
|
suf = _ui_t_high(suf)
|
|
return uiosi, uinhosts, uiupdates, uiuptimes, uinpdates, uinptimes, suf
|
|
|
|
for osi in nat_sorted(osdata['hosts']):
|
|
if '/' not in osi:
|
|
if len(osdata['vers'][osi]) == 1:
|
|
subprefix = ''
|
|
subplen = 14
|
|
continue
|
|
else: # Have level 2 be local to the group.
|
|
max_nhosts_lvl_2 = 0
|
|
max_update_lvl_2 = 0
|
|
max_uptime_lvl_2 = 0
|
|
for sosi in osdata['vers'][osi]:
|
|
supd = osdata['updates'][sosi] / osdata['hosts'][sosi]
|
|
supt = osdata['uptimes'][sosi] / osdata['hosts'][sosi]
|
|
max_nhosts_lvl_2 = max(max_nhosts_lvl_2, osdata['hosts'][sosi])
|
|
max_update_lvl_2 = max(max_update_lvl_2, supd)
|
|
max_uptime_lvl_2 = max(max_uptime_lvl_2, supt)
|
|
subprefix = ' '
|
|
subplen = 12
|
|
updt = _ui_up_d_t(osi, 1)
|
|
uiosi = updt[0]
|
|
uinhosts = updt[1]
|
|
uiupdates = updt[2]
|
|
uiuptimes = updt[3]
|
|
uinpdates = updt[4]
|
|
uinptimes = updt[5]
|
|
print(" %s: %s %s %s %s %s%s" % (uiosi, uinhosts,
|
|
uiupdates, uiuptimes, uinpdates, uinptimes, updt[6]))
|
|
if '/' in osi:
|
|
if subplen == 14: # Hack to say are we level 1
|
|
updt = _ui_up_d_t(osi, 1)
|
|
else:
|
|
updt = _ui_up_d_t(osi, 2)
|
|
uiosi = updt[0]
|
|
uinhosts = updt[1]
|
|
uiupdates = updt[2]
|
|
uiuptimes = updt[3]
|
|
uinpdates = updt[4]
|
|
uinptimes = updt[5]
|
|
print(" %s%s: %s %s %s %s %s%s" % (subprefix, uiosi, uinhosts,
|
|
uiupdates, uiuptimes, uinpdates, uinptimes, updt[6]))
|
|
print("-" * (16+2+6+2*(1+_max_len_rpms+1+_max_len_upts)))
|
|
# Redo the lengths, because it's real hostname data now...
|
|
_max_update(data)
|
|
_max_len_date = 0
|
|
_max_update_correct(' ')
|
|
if most:
|
|
# print("")
|
|
print("Hosts with the most", _ui_t_key("Updates") + ":")
|
|
for m in most:
|
|
print(" %-*s %*s %*s %s" % (_max_len_name, m[2],
|
|
_max_len_rpms, _ui_int(m[0]), _max_len_upts, _ui_dur(m[1]),
|
|
_ui_osinfo(m[2])))
|
|
if awakest:
|
|
# print("")
|
|
print("Hosts with the most", _ui_t_key("Uptime") + ":")
|
|
for a in awakest:
|
|
print(" %-*s %*s %*s %s" % (_max_len_name, a[2],
|
|
_max_len_rpms, _ui_int(a[1]), _max_len_upts, _ui_dur(a[0]),
|
|
_ui_osinfo(a[2])))
|
|
_explain_ui_name()
|
|
|
|
def _print_info(hosts, data):
|
|
fhosts = []
|
|
for x in data:
|
|
for host in hosts:
|
|
if not _fnmatchi(x.name, host):
|
|
continue
|
|
fhosts.append(x)
|
|
break
|
|
if not fhosts:
|
|
print("Not host(s) matched:", host)
|
|
sys.exit(2)
|
|
for host in fhosts:
|
|
print("Host:", host.name)
|
|
print(" OS:", host.osinfo)
|
|
print(" Updates:", _ui_int(host.rpms))
|
|
print(" Uptime:", format_duration(host.uptime)) # !ui_dur
|
|
print(" Checked:", _ui_date(host))
|
|
if conf_info_machine_ids:
|
|
print(" Machine:", host.machine_id)
|
|
print(" Boot:", host.boot_id)
|
|
|
|
def _cmd_info(args):
|
|
hists = ['main']
|
|
hosts = conf_important_hosts.copy()
|
|
if args.host:
|
|
# print("JDBG:", args.host)
|
|
hosts = [args.host]
|
|
if False and args.all:
|
|
for b in backups:
|
|
print("History:", b)
|
|
_print_info(hosts, lines2datas(bfname2lines(b)))
|
|
if args.hists:
|
|
hists = args.hists[:]
|
|
for hist in hists:
|
|
if hist != "main": # One or more historical files...
|
|
print("History:", hist)
|
|
else:
|
|
print("Main:")
|
|
_print_info(hosts, fname1([hist]))
|
|
|
|
# If save=True, and we haven't output anything then save the line as we might
|
|
# not do anything. After something has gone out save=True does nothing.
|
|
_prnt_line_saved = []
|
|
def _print_line_add(line):
|
|
global _prnt_line_saved
|
|
if _prnt_line_saved is None:
|
|
_prnt_line_saved = []
|
|
_prnt_line_saved.append(line)
|
|
def _print_line_reset():
|
|
global _prnt_line_saved
|
|
ret = _prnt_line_saved is not None
|
|
_prnt_line_saved = []
|
|
return ret
|
|
def _print_line(prefix, data, high='', save=False, prev=None):
|
|
global _prnt_line_saved
|
|
if prev is not None:
|
|
prev = prev.date
|
|
uiname = "%-*s" % (_max_len_name, _ui_name(data.name))
|
|
if high:
|
|
uiname = _ui_t_ansi(uiname, high)
|
|
line = "%s%s %*s %*s %s %s" % (prefix,
|
|
uiname,
|
|
_max_len_rpms, _ui_int(data.rpms),
|
|
_max_len_upts, _ui_dur(data.uptime),
|
|
_ui_date(data, align=_max_len_date, prev=prev), _ui_osinfo(data))
|
|
if save and _prnt_line_saved is not None:
|
|
_prnt_line_saved.append(line)
|
|
return
|
|
if _prnt_line_saved is not None:
|
|
for oline in _prnt_line_saved:
|
|
print(oline)
|
|
_prnt_line_saved = None
|
|
print(line)
|
|
|
|
def _print_lines(prefix, data, explain=True):
|
|
pd1 = None
|
|
for d1 in data:
|
|
_print_line(prefix, d1, prev=pd1)
|
|
pd1 = d1
|
|
if explain:
|
|
_explain_ui_name()
|
|
|
|
# -n variants match multiple things, but only allow looking at current data
|
|
def _cmd_list(args):
|
|
# FIXME: Ideally argparse would do this for us :(
|
|
hists = []
|
|
hosts = []
|
|
osnames = []
|
|
if hasattr(args, 'hist') and args.hist is not None:
|
|
hists = [args.hist]
|
|
if hasattr(args, 'hosts'):
|
|
hosts = args.hosts[:]
|
|
if hasattr(args, 'osnames'):
|
|
osnames = args.osnames[:]
|
|
if hasattr(args, 'host') and args.host is not None:
|
|
hosts += [args.host]
|
|
if hasattr(args, 'osname') and args.osname is not None:
|
|
osnames += [args.osname]
|
|
|
|
data = fname1(hists)
|
|
if hasattr(args, 'dateage'):
|
|
data = list(filter_age_min_datas(data, args.dateage))
|
|
data = list(filter_name_datas(data, hosts))
|
|
data = list(filter_osname_datas(data, osnames))
|
|
_max_update(data)
|
|
_max_update_correct('')
|
|
print(_ui_t_title("Host", -_max_len_name),
|
|
_ui_t_title("*", _max_len_rpms),
|
|
_ui_t_title("Up", _max_len_upts),
|
|
_ui_t_title("Date", _max_len_date), _ui_t_title("OS"))
|
|
_print_lines('', data)
|
|
|
|
def _cmd_uptime(args):
|
|
age = 0
|
|
if args.dur > 0:
|
|
age = args.dur
|
|
|
|
data = fname1()
|
|
if cmd == "uptime-max" or cmd in _cmds_als["uptime-max"]:
|
|
data = list(filter_uptime_max_datas(data, age))
|
|
else:
|
|
data = list(filter_uptime_min_datas(data, age))
|
|
_max_update(data)
|
|
_max_update_correct('')
|
|
_print_lines('', data)
|
|
|
|
def _cmd_created(args):
|
|
age = 0
|
|
if args.dur > 0:
|
|
age = args.dur
|
|
|
|
hosts = []
|
|
if hasattr(args, 'hosts'):
|
|
hosts = args.hosts[:]
|
|
|
|
data = fname1()
|
|
if age > 0: # If a host has been running longer, then ignore it
|
|
data = filter_uptime_max_datas(data, age)
|
|
if hosts:
|
|
data = filter_name_datas(data, hosts)
|
|
|
|
hn2h = {} # hostnames to host ... for machine ids
|
|
for host in data:
|
|
hn2h[host.name] = host
|
|
|
|
hbackups = list(reversed(backups))
|
|
if len(hbackups) > 1 and _backup_today_identical():
|
|
hbackups = hbackups[1:]
|
|
|
|
rei = set() # (re)installed hosts...
|
|
dur = 0
|
|
for backup in hbackups:
|
|
if age > 0 and age < dur:
|
|
break
|
|
dur += 60*60*24 # Broken, but mostly works. Do backups daily, or mtime.
|
|
|
|
data = lines2datas(bfname2lines(backup))
|
|
data = list(sorted(data))
|
|
|
|
# If a machine is missing from an older backup, it's marked as being
|
|
# (re)installed ... even though it might appear in an even older
|
|
# backup with the same machine_id. It is what it is.
|
|
dnames = set(x.name for x in data)
|
|
for hostname in hn2h:
|
|
if hostname not in dnames:
|
|
rei.add(hostname)
|
|
|
|
for host in data:
|
|
if host.name not in hn2h: # Ignore
|
|
continue
|
|
if host.name in rei: # Most recent (re)install
|
|
continue
|
|
|
|
if hn2h[host.name].machine_id != host.machine_id:
|
|
rei.add(host.name)
|
|
else: # Show the oldest date...
|
|
hn2h[host.name].date = host.date
|
|
|
|
data = []
|
|
for hostname in rei:
|
|
data.append(hn2h[hostname])
|
|
data = sorted(data)
|
|
|
|
_max_update(data)
|
|
_max_update_correct('')
|
|
_print_lines('', data)
|
|
|
|
def _diff_hosts(data1, data2, show_both=False, show_utf8=True, skip_eq=False):
|
|
pdata = None
|
|
while len(data1) > 0 or len(data2) > 0:
|
|
if len(data1) <= 0:
|
|
_print_line('+', data2[0], prev=pdata)
|
|
pdata = data2.pop(0)
|
|
continue
|
|
if len(data2) <= 0:
|
|
_print_line('-', data1[0], prev=pdata)
|
|
pdata = data1.pop(0)
|
|
continue
|
|
|
|
d1 = data1[0]
|
|
d2 = data2[0]
|
|
|
|
if d1.name < d2.name:
|
|
_print_line('-', d1, prev=pdata)
|
|
pdata = data1.pop(0)
|
|
continue
|
|
|
|
if d1.name > d2.name:
|
|
_print_line('+', d2, prev=pdata)
|
|
pdata = data2.pop(0)
|
|
continue
|
|
|
|
# d1.name == d2.name; so both are going now
|
|
data1.pop(0)
|
|
data2.pop(0)
|
|
|
|
# Name, rpms, and OSname/OSvers are the same
|
|
if d1 == d2:
|
|
if show_utf8 and conf_utf8 and host_rebooted(d1, d2):
|
|
u_ed_or_up = _conf_utf8_boot_ed
|
|
h_ed_or_up = conf_term_host_boot_ed
|
|
if not d2.rpms:
|
|
u_ed_or_up = _conf_utf8_boot_up
|
|
h_ed_or_up = conf_term_host_boot_up
|
|
_print_line(u_ed_or_up, d2, high=h_ed_or_up, prev=pdata)
|
|
pdata = d2
|
|
continue
|
|
_print_line(' ', d2, save=skip_eq, prev=pdata)
|
|
pdata = d2
|
|
continue
|
|
|
|
# Something changed, see what and set utf8 prefix and highlight
|
|
if False: pass
|
|
elif not _wild_eq(d1.machine_id, d2.machine_id):
|
|
utf8, high = _conf_utf8_diff_hw, conf_term_host_diff_hw
|
|
elif not _wild_info_eq(d1, d2):
|
|
utf8, high = _conf_utf8_diff_os, conf_term_host_diff_os
|
|
elif host_rebooted(d1, d2) and d1.rpms > d2.rpms:
|
|
utf8, high = _conf_utf8_boot_up, conf_term_host_boot_up
|
|
elif host_rebooted(d1, d2):
|
|
utf8, high = _conf_utf8_boot_ed, conf_term_host_boot_ed
|
|
elif d1.rpms > d2.rpms:
|
|
utf8, high = _conf_utf8_less_up, conf_term_host_less_up
|
|
else: # d1.rpms < d2.rpms:
|
|
utf8, high = _conf_utf8_more_up, conf_term_host_more_up
|
|
|
|
if not conf_utf8:
|
|
utf8 = '+'
|
|
|
|
# Something about host changed, show old/new...
|
|
if show_both:
|
|
_print_line('-', d1, prev=pdata)
|
|
_print_line(utf8, d2, high=high, prev=d1)
|
|
pdata = d2
|
|
continue
|
|
|
|
# Something changed, but we only show the new data...
|
|
if not conf_utf8:
|
|
utf8 = '!'
|
|
_print_line(utf8, d2, high=high, prev=pdata)
|
|
pdata = d2
|
|
|
|
def _cmd_diff(args):
|
|
hists = args.hists[:]
|
|
if len(hists) < 1:
|
|
hists = ["diff-latest", 'main']
|
|
if len(hists) < 2:
|
|
hists += ['main']
|
|
data1 = fname1(hists)
|
|
fn1 = hists.pop(0)
|
|
data2 = fname1(hists)
|
|
fn2 = hists.pop(0)
|
|
print(_ui_t_cmd("diff %s %s" % (fn1, fn2)), file=sys.stderr)
|
|
data1 = list(sorted(data1))
|
|
data2 = list(sorted(data2))
|
|
hosts = _ui_int(len(data2))
|
|
data1 = list(filter_osname_datas(data1, args.osnames))
|
|
data2 = list(filter_osname_datas(data2, args.osnames))
|
|
updates = _ui_int(sum(d.rpms for d in data2))
|
|
ul = len(updates)
|
|
cmpds = _ui_diffstats(data1, data2)
|
|
_max_update(data1)
|
|
_max_update(data2)
|
|
_max_update_correct(' ')
|
|
|
|
_cmdn_du = cmd in ("difference-u", "diff-u")
|
|
_diff_hosts(data1, data2, show_both=_cmdn_du, show_utf8=not _cmdn_du)
|
|
|
|
_print_diffstats(hosts, updates, cmpds)
|
|
_explain_ui_name()
|
|
|
|
# Like diff/history mixed, but for specific hosts...
|
|
def _cmd_host(args):
|
|
global conf_host_skip_eq
|
|
|
|
hosts = conf_important_hosts.copy()
|
|
if args.hosts:
|
|
hosts = args.hosts
|
|
|
|
print("Hosts history:")
|
|
last_name = "main"
|
|
last_data = list(sorted(filter_name_datas(fname_datas(), hosts)))
|
|
last_data = list(filter_osname_datas(last_data, args.osnames))
|
|
|
|
hbackups = list(reversed(backups))
|
|
if len(hbackups) > 1 and _backup_today_identical():
|
|
hbackups = hbackups[1:]
|
|
|
|
_max_update(last_data)
|
|
if not conf_fast_width_history:
|
|
# Would be a lot faster if we could exit early here, but it's difficult
|
|
for backup in hbackups:
|
|
data = filter_name_datas(lines2datas(bfname2lines(backup)), hosts)
|
|
data = list(sorted(data))
|
|
data = list(filter_osname_datas(data, args.osnames))
|
|
_max_update(data)
|
|
_max_update_correct(' ')
|
|
|
|
if conf_utf8:
|
|
sep = conf_host_arrow_utf8
|
|
else:
|
|
sep = conf_host_arrow_asci
|
|
done = False
|
|
skipped_num = 0
|
|
thostnum = 0
|
|
for backup in hbackups:
|
|
data = filter_name_datas(lines2datas(bfname2lines(backup)), hosts)
|
|
data = list(sorted(data))
|
|
data = list(filter_osname_datas(data, args.osnames))
|
|
|
|
if done and skipped_num < 1:
|
|
print("")
|
|
done = True
|
|
skipped = sep + ' '
|
|
if skipped_num >= 1:
|
|
# Use "skip(s)" so the alignment works out, for 1-9 skips.
|
|
skipped = "%s %s skip(s) %s " % (sep, skipped_num, sep)
|
|
|
|
_print_line_add("Host diff: %s %s%s" % (backup, skipped,
|
|
_ui_t_time(last_name)))
|
|
if conf_fast_width_history:
|
|
_max_update(data)
|
|
_max_update_correct(' ')
|
|
if skipped and backup == backups[0]:
|
|
conf_host_skip_eq = False
|
|
_diff_hosts(data.copy(), last_data.copy(), show_both=cmd.endswith("-u"),
|
|
skip_eq=conf_host_skip_eq)
|
|
lines_output = not _print_line_reset()
|
|
hostnum = len(last_data)
|
|
if lines_output and conf_host_show_diffstat_hostnum <= hostnum:
|
|
cmpds = _ui_diffstats(data, last_data)
|
|
updates = _ui_int(sum(d.rpms for d in last_data))
|
|
_print_diffstats(hostnum, updates, cmpds)
|
|
thostnum += 1
|
|
|
|
if not lines_output:
|
|
skipped_num += 1
|
|
continue
|
|
|
|
last_name = backup
|
|
last_data = data
|
|
skipped_num = 0
|
|
thostnum += 2
|
|
|
|
if conf_host_end_total_hostnum > 0:
|
|
thostnum += hostnum
|
|
if conf_host_end_total_hostnum <= thostnum:
|
|
sys.exit(0) # Don't want to output "host data".
|
|
|
|
if done:
|
|
print("")
|
|
print("Host data: %s" % (_ui_t_time(last_name),), file=sys.stderr)
|
|
_print_lines(' ', last_data)
|
|
|
|
|
|
def _cmdline_arg_ansi(oval):
|
|
val = oval.lower()
|
|
if val in ("yes", "on", "1", "always"):
|
|
return True
|
|
if val in ("no", "off", "0", "never"):
|
|
return False
|
|
if val in ("automatic", "?", "tty", "auto"):
|
|
return None
|
|
raise argparse.ArgumentTypeError(f"{oval} is not valid: always/never/auto")
|
|
|
|
def _cmdline_arg_duration(oval):
|
|
if oval == "forever":
|
|
return 0
|
|
val = parse_duration(oval)
|
|
if val is None:
|
|
raise argparse.ArgumentTypeError(f"{oval} is not a duration")
|
|
return val
|
|
|
|
def _cmdline_arg_positive_integer(oval):
|
|
try:
|
|
val = int(oval)
|
|
except:
|
|
val = -1
|
|
if val <= 0:
|
|
raise argparse.ArgumentTypeError(f"{oval} is not a positive integer")
|
|
return val
|
|
|
|
def _cmdline_arg_hist(oval):
|
|
# The big problem here is that this happens before conf_* is fully loaded
|
|
# so we can't trust backups[] yet. So defer everything until fname1().
|
|
names = ("main", "latest", "newest", "diff-latest", "diff-newest", "oldest",
|
|
"today", "yesterday")
|
|
if oval in names:
|
|
return oval
|
|
|
|
if oval == "today": # Special ... might be nice to error on these, sigh...
|
|
if fname_today is not None:
|
|
return backup_today
|
|
raise argparse.ArgumentTypeError(f"No history file for today")
|
|
if oval == "yesterday" and fname_yesterday is not None:
|
|
if fname_yesterday is not None:
|
|
return backup_yesterday
|
|
raise argparse.ArgumentTypeError(f"No history file for yesterday")
|
|
|
|
if oval in backups:
|
|
return oval
|
|
|
|
msg = f"{oval} is not a history file"
|
|
msg += "\n History:", ", ".join([] + names + backups)
|
|
raise argparse.ArgumentTypeError(msg)
|
|
|
|
_cmds_als = {
|
|
"created" : ["installed", "reinstalled"],
|
|
"difference" : ("diff", "diff-u", "difference-u"),
|
|
"history-keep" : [],
|
|
"history" : ["hist"],
|
|
"hosts" : ("host", "host-u", "hosts-u"),
|
|
"information" : ["info"],
|
|
"list" : ["list-n"],
|
|
"old-list" : ["olist"],
|
|
"oslist" : ["oslist-n"],
|
|
"statistics" : ["stats"],
|
|
"update" : [],
|
|
"update-daily" : [],
|
|
"update-daily-refresh" : [],
|
|
"update-fast" : [],
|
|
"update-host" : [],
|
|
"uptime-min" : ["uptime"],
|
|
"uptime-max" : ("rebooted", "started"),
|
|
None : set(),
|
|
}
|
|
for c in _cmds_als:
|
|
if c is None: continue
|
|
_cmds_als[c] = set(_cmds_als[c])
|
|
_cmds_als[None].update(_cmds_als[c])
|
|
_cmds_als[None].add(c)
|
|
|
|
def _cmd_help(args):
|
|
prog = "updates+uptime"
|
|
if sys.argv:
|
|
prog = os.path.basename(sys.argv[0])
|
|
if not args.hcmd:
|
|
_usage()
|
|
if args.hcmd not in _cmds_als[None]:
|
|
print(" Unknown command:", args.hcmd)
|
|
_usage()
|
|
|
|
def _eq_cmd(x):
|
|
return args.hcmd == x or args.hcmd in _cmds_als[x]
|
|
def _hlp_als(x):
|
|
if not _cmds_als[x]:
|
|
return ''
|
|
als = set()
|
|
als.add(x)
|
|
als.update(_cmds_als[x])
|
|
als.remove(args.hcmd)
|
|
return f"""Aliases: {", ".join(sorted(als))}\n"""
|
|
|
|
if _eq_cmd("created"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd} [duration] [host*]
|
|
|
|
See when machine_id last changed. This happens when they are created/installed
|
|
or reinstalled. Can be filtered for time <= duration. Zero duration means
|
|
forever.
|
|
|
|
Easy way to see new machines.
|
|
|
|
{_hlp_als("created")}
|
|
Eg. {prog} {args.hcmd} 0 *-test*
|
|
{prog} {args.hcmd} 5w vmhost* bvmhost* buildhw*
|
|
""", end='')
|
|
|
|
elif _eq_cmd("difference"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd} [backup1] [backup2]
|
|
{' '*len(prog)} {' '*len(args.hcmd)} --osnames os*
|
|
|
|
See the difference between the current state and backups.
|
|
The -u variant shows before/after instead of modified.
|
|
|
|
utf8: {conf_utf8}
|
|
{_conf_utf8_boot_ed} = Rebooted
|
|
{_conf_utf8_boot_up} = Rebooted and updated
|
|
{_conf_utf8_more_up} = More updates
|
|
{_conf_utf8_less_up} = Less updates
|
|
{_conf_utf8_diff_os} = Different OS information, but machine id is the same
|
|
{_conf_utf8_diff_hw} = Machine id is different
|
|
|
|
{_hlp_als("difference")}
|
|
Eg. {prog} {args.hcmd}
|
|
{prog} {args.hcmd} yesterday
|
|
{prog} {args.hcmd} --osnames 42 2025-08-16 main
|
|
""", end='')
|
|
|
|
elif _eq_cmd("history"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd}
|
|
{' '*len(prog)} {' '*len(args.hcmd)} --osnames os*
|
|
|
|
Show summary of current data, and how it changed over time.
|
|
|
|
Kind of like: git log --pretty=oneline --abbrev-commit --decorate
|
|
|
|
{_hlp_als("history")}
|
|
Eg. {prog} {args.hcmd}
|
|
""", end='')
|
|
|
|
elif _eq_cmd("history-keep"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd} [days]
|
|
|
|
Cleanup old history, older than the given number of days.
|
|
The main file and today's history have to be kept, which happens
|
|
if you pass "1".
|
|
|
|
Logrotate, for history.
|
|
|
|
{_hlp_als("history-keep")}
|
|
Eg. {prog} {args.hcmd}
|
|
{prog} {args.hcmd} 32
|
|
""", end='')
|
|
|
|
elif _eq_cmd("hosts"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd}
|
|
{' '*len(prog)} {' '*len(args.hcmd)} --osnames os*
|
|
|
|
See the history of a host(s). A cross between looking at diff and history.
|
|
The -u variants show before/after instead of modified.
|
|
|
|
Easiest way to see how hosts have changed over time, the more history the
|
|
better for this. Kind of like git blame, but instead of lines it's events.
|
|
|
|
utf8: {conf_utf8}
|
|
{_conf_utf8_boot_ed} = Rebooted
|
|
{_conf_utf8_boot_up} = Rebooted and updated
|
|
{_conf_utf8_more_up} = More updates
|
|
{_conf_utf8_less_up} = Less updates
|
|
{_conf_utf8_diff_os} = Different OS information, but machine id is the same
|
|
{_conf_utf8_diff_hw} = Machine id is different
|
|
|
|
{_hlp_als("hosts")}
|
|
Eg. {prog} {args.hcmd}
|
|
{prog} {args.hcmd} 'batcave*'
|
|
{prog} {args.hcmd} 'batcave*' 'noc*'
|
|
""", end='')
|
|
|
|
elif _eq_cmd("information"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd} [host*] [backup] [backup]...
|
|
|
|
See the current state, in long form, can be filtered by name.
|
|
|
|
If you want to compare things by hand, use this.
|
|
|
|
{_hlp_als("information")}
|
|
Eg. {prog} {args.hcmd}
|
|
{prog} {args.hcmd} 'batcave*'
|
|
{prog} {args.hcmd} 'noc*' main yesterday
|
|
""", end='')
|
|
|
|
elif _eq_cmd("list"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd} [host*] [host*]...
|
|
{' '*len(prog)} {' '*len(args.hcmd)} --history backup
|
|
{' '*len(prog)} {' '*len(args.hcmd)} --osnames os*
|
|
|
|
See the current state of the hosts, can be filtered by name.
|
|
|
|
{_hlp_als("list")}
|
|
Eg. {prog} {args.hcmd}
|
|
{prog} {args.hcmd} 'batcave*'
|
|
{prog} {args.hcmd} 'batcave*' 'noc*'
|
|
{prog} {args.hcmd} --osnames '4?' '*stg*' '*test*'
|
|
""", end='')
|
|
|
|
elif _eq_cmd("old-list"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd} duration
|
|
{' '*len(prog)} {' '*len(args.hcmd)} --osnames os*
|
|
|
|
See the current state of hosts, with data older than duration.
|
|
|
|
Easiest way to see what hosts we aren't getting data for.
|
|
|
|
{_hlp_als("old-list")}
|
|
Eg. {prog} {args.hcmd} 2d
|
|
{prog} {args.hcmd} --osnames 42 2d
|
|
""", end='')
|
|
|
|
elif _eq_cmd("oslist"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd} [os*] [os*]...
|
|
{' '*len(prog)} {' '*len(args.hcmd)} --history backup
|
|
|
|
See the current state, can be filtered by OS.
|
|
|
|
{_hlp_als("oslist")}
|
|
Eg. {prog} {args.hcmd}
|
|
{prog} {args.hcmd} RedHat
|
|
{prog} {args.hcmd} F 10
|
|
{prog} {args.hcmd} 10 --history yesterday 41
|
|
""", end='')
|
|
|
|
elif _eq_cmd("statistics"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd} [backup] [host*] [host*]...
|
|
{' '*len(prog)} {' '*len(args.hcmd)} --history backup
|
|
|
|
Show general statistics.
|
|
|
|
Easiest way to see the current state of the hosts.
|
|
|
|
{_hlp_als("statistics")}
|
|
Eg. {prog} {args.hcmd}
|
|
{prog} {args.hcmd} --history yesterday
|
|
{prog} {args.hcmd} '*.stg.*' '*-test.*'
|
|
""", end='')
|
|
|
|
elif _eq_cmd("update"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd}
|
|
|
|
Run update-fast, or update-daily if no daily backup.
|
|
|
|
Easiest way to update, from cron or cmdline after you change things. DTRT.
|
|
Hosts removed from inventory are automatically removed on {args.hcmd}.
|
|
|
|
{_hlp_als("update")}
|
|
Eg. {prog} {args.hcmd}
|
|
""", end='')
|
|
|
|
elif _eq_cmd("update-daily"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd}
|
|
|
|
Run update-fast and force do a backup for today.
|
|
Hosts removed from inventory are automatically removed on {args.hcmd}.
|
|
|
|
{_hlp_als("update-daily")}
|
|
Eg. {prog} {args.hcmd}
|
|
""", end='')
|
|
|
|
elif _eq_cmd("update-daily-refresh"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd}
|
|
|
|
Delete the current file, then run update and also force do a backup for today.
|
|
|
|
Easiest way to refresh the current history for today.
|
|
|
|
{_hlp_als("update-daily-refresh")}
|
|
Eg. {prog} {args.hcmd}
|
|
""", end='')
|
|
|
|
elif _eq_cmd("update-fast"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd}
|
|
|
|
Update the data for the hosts, in the main file (creating it if needed).
|
|
Hosts removed from inventory are automatically removed on {args.hcmd}.
|
|
|
|
{_hlp_als("update-fast")}
|
|
Eg. {prog} {args.hcmd}
|
|
""", end='')
|
|
|
|
elif _eq_cmd("update-host"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd} host*
|
|
|
|
Run update-fast, only for the specified host(s).
|
|
Hosts removed from inventory are automatically removed on {args.hcmd}.
|
|
|
|
{_hlp_als("update-host")}
|
|
Eg. {prog} {args.hcmd} bat\\*
|
|
""", end='')
|
|
|
|
elif _eq_cmd("uptime-min"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd} duration [backup]
|
|
|
|
See the current state, can be filtered for uptime >= duration.
|
|
|
|
Easy way to see what has been rebooted recently.
|
|
|
|
{_hlp_als("uptime-min")}
|
|
Eg. {prog} {args.hcmd} 32h
|
|
{prog} {args.hcmd} 1d yesterday
|
|
""", end='')
|
|
|
|
elif _eq_cmd("uptime-max"):
|
|
print(f"""\
|
|
Usage: {prog} {args.hcmd} duration [backup]
|
|
|
|
See the current state, can be filtered for uptime <= duration.
|
|
|
|
Easy way to see what hasn't been rebooted recently.
|
|
|
|
{_hlp_als("uptime-max")}
|
|
Eg. {prog} {args.hcmd} 26w
|
|
{prog} {args.hcmd} 4w4d yesterday
|
|
""", end='')
|
|
|
|
|
|
def _main():
|
|
global conf_ansi_terminal
|
|
global conf_path
|
|
global cmd
|
|
|
|
_user_conf()
|
|
|
|
parser = argparse.ArgumentParser(add_help=False)
|
|
parser.add_argument('--verbose', '-v', action='count', default=0)
|
|
parser.add_argument("--conf", action='append', default=[])
|
|
parser.add_argument("--db-dir")
|
|
parser.add_argument("--ansi", type=_cmdline_arg_ansi,
|
|
help="Use ansi terminal codes")
|
|
parser.add_argument("--colour", type=_cmdline_arg_ansi, dest='ansi',
|
|
help=argparse.SUPPRESS)
|
|
parser.add_argument("--color", type=_cmdline_arg_ansi, dest='ansi',
|
|
help=argparse.SUPPRESS)
|
|
parser.add_argument('-h', '--help', action='store_true',
|
|
help='Show this help message')
|
|
|
|
# We do this here so that `$0 -v stats -v` works.
|
|
margs, args = parser.parse_known_args()
|
|
|
|
if margs.help:
|
|
_usage(short=True)
|
|
sys.exit(0)
|
|
|
|
subparsers = parser.add_subparsers(dest="cmd")
|
|
|
|
cmd = subparsers.add_parser("help")
|
|
cmd.add_argument("hcmd", nargs='?', help="cmd to get help for")
|
|
cmd.set_defaults(func=_cmd_help)
|
|
|
|
def __defs(func):
|
|
cmd.set_defaults(func=func)
|
|
|
|
# parser.add_argument('rest', nargs='*', type=int)
|
|
|
|
# HIDDEN commands...
|
|
cmd = subparsers.add_parser("dur2secs", help=argparse.SUPPRESS)
|
|
cmd.add_argument("dur", type=_cmdline_arg_duration, help="duration")
|
|
__defs(func=lambda x: print("secs:", x.dur))
|
|
|
|
cmd = subparsers.add_parser("secs2dur", help=argparse.SUPPRESS)
|
|
cmd.add_argument("secs", type=int, help="seconds")
|
|
__defs(func=lambda x: print("dur:", _ui_dur(x.secs, short=False)))
|
|
|
|
cmd = subparsers.add_parser("int2num", help=argparse.SUPPRESS)
|
|
cmd.add_argument("num", type=int, help="int")
|
|
__defs(func=lambda x: print("num:", _ui_int(x.num)))
|
|
|
|
# -- Start of the real commands...
|
|
|
|
als = _cmds_als["created"]
|
|
cmd = subparsers.add_parser("created", aliases=als, help="list hosts")
|
|
cmd.add_argument("dur", nargs='?', default=conf_created_dur_def,
|
|
type=_cmdline_arg_duration, help="created within duration")
|
|
cmd.add_argument("hosts", nargs='*', help="wildcard hostname(s)")
|
|
__defs(func=_cmd_created)
|
|
|
|
# diff/diff-u commands
|
|
als = _cmds_als["difference"]
|
|
cmd = subparsers.add_parser("difference", aliases=als, help="diff")
|
|
cmd.add_argument("--osnames", action='append', default=[], help="wildcard OSname(s)")
|
|
cmd.add_argument("hists", nargs='*', type=_cmdline_arg_hist, help="history file")
|
|
__defs(func=_cmd_diff)
|
|
|
|
# hosts/hosts-u commands
|
|
als = _cmds_als["hosts"]
|
|
hlp = "show history data about specific hosts"
|
|
cmd = subparsers.add_parser("hosts", aliases=als, help=hlp)
|
|
cmd.add_argument("--osnames", action='append', default=[], help="wildcard OSname(s)")
|
|
cmd.add_argument("hosts", nargs='*', help="wildcard hostname(s)")
|
|
__defs(func=_cmd_host)
|
|
|
|
# history command
|
|
als = _cmds_als["history"]
|
|
cmd = subparsers.add_parser("history", aliases=als, help="show history")
|
|
cmd.add_argument("--osnames", action='append', default=[], help="wildcard OSname(s)")
|
|
cmd.add_argument("hosts", nargs='*', help="wildcard hostname(s)")
|
|
__defs(func=_cmd_history)
|
|
|
|
# history-keep command
|
|
cmd = subparsers.add_parser("history-keep", help="remove old files")
|
|
hlp = "number of history files to keep"
|
|
cmd.add_argument("keep", nargs='?', default=conf_history_keep_def,
|
|
type=_cmdline_arg_positive_integer, help=hlp)
|
|
__defs(func=_cmd_history_keep)
|
|
|
|
# info command
|
|
als = _cmds_als["information"]
|
|
hlp = "show host information"
|
|
cmd = subparsers.add_parser("information", aliases=als, help=hlp)
|
|
cmd.add_argument("host", nargs='?', help="wildcard hostname")
|
|
cmd.add_argument("hists", nargs='*',
|
|
type=_cmdline_arg_hist, help="history file")
|
|
__defs(func=_cmd_info)
|
|
|
|
# list/list-n/old-list/olist/oslist/oslist-n commands
|
|
cmd = subparsers.add_parser("list", help="list hosts")
|
|
cmd.add_argument("--osnames", action='append', default=[], help="wildcard OSname(s)")
|
|
cmd.add_argument("--history", dest="hist",
|
|
type=_cmdline_arg_hist, help="history file")
|
|
cmd.add_argument("hosts", nargs='*', help="wildcard hostname(s)")
|
|
__defs(func=_cmd_list)
|
|
|
|
als = _cmds_als["old-list"]
|
|
cmd = subparsers.add_parser("old-list", aliases=als, help="list hosts")
|
|
cmd.add_argument("--osnames", action='append', default=[], help="wildcard OSname(s)")
|
|
cmd.add_argument("dateage", type=_cmdline_arg_duration,
|
|
help="data age minimum duration")
|
|
__defs(func=_cmd_list)
|
|
|
|
cmd = subparsers.add_parser("oslist", help="list hosts")
|
|
cmd.add_argument("--history", dest="hist",
|
|
type=_cmdline_arg_hist, help="history file")
|
|
cmd.add_argument("osnames", nargs='*', help="wildcard OSname(s)")
|
|
__defs(func=_cmd_list)
|
|
|
|
# stats commands
|
|
hlp = "show stats information"
|
|
cmd = subparsers.add_parser("statistics", aliases=['stats'], help=hlp)
|
|
cmd.add_argument("--history", dest="hist", type=_cmdline_arg_hist,
|
|
default="main", help="history file")
|
|
cmd.add_argument("hosts", nargs='*', help="wildcard hostname(s)")
|
|
__defs(func=_cmd_stats)
|
|
|
|
# update commands
|
|
# We pretend these are sep. commands, like oslist vs. list, but don't C&P.
|
|
# als = _cmds_als["update"]
|
|
als = ['update-daily','update-daily-refresh', 'update-fast', 'update-flush']
|
|
cmd = subparsers.add_parser("update", aliases=als, help="update DB")
|
|
__defs(func=_cmd_update)
|
|
|
|
cmd = subparsers.add_parser("update-host", help="update DB for hosts")
|
|
cmd.add_argument("host", help="wildcard hostname")
|
|
__defs(func=_cmd_update)
|
|
|
|
cmd = subparsers.add_parser("remove-old-hosts", help="update DB")
|
|
__defs(func=_cmd_remove_old_hosts)
|
|
|
|
# uptime/uptime-min/uptime-max commands
|
|
als = _cmds_als["uptime-max"]
|
|
cmd = subparsers.add_parser("uptime-max", aliases=als, help="list hosts")
|
|
cmd.add_argument("dur", type=_cmdline_arg_duration,
|
|
help="uptime maximum duration")
|
|
__defs(func=_cmd_uptime)
|
|
|
|
als = _cmds_als["uptime-min"]
|
|
cmd = subparsers.add_parser("uptime-min", help="list hosts", aliases=als)
|
|
cmd.add_argument("dur", type=_cmdline_arg_duration,
|
|
help="uptime minimum duration")
|
|
__defs(func=_cmd_uptime)
|
|
|
|
# Need to setup backup[] for cmd line validation ... but conf can change
|
|
# so just validate format? And revalidate later?
|
|
_pre_cmd__setup()
|
|
|
|
# Parse the above options/cmds
|
|
args = parser.parse_args(args)
|
|
|
|
if margs.db_dir:
|
|
conf_path = margs.db_dir
|
|
|
|
for line in margs.conf:
|
|
_user_conf_line(line)
|
|
|
|
if margs.ansi is not None:
|
|
conf_ansi_terminal = margs.ansi
|
|
|
|
# Setup based on the config.
|
|
_pre_cmd__setup()
|
|
|
|
_pre_cmd__verbose(margs)
|
|
|
|
_pre_cmd__check_paths()
|
|
|
|
# Run the actual command.
|
|
if not hasattr(args, "func"):
|
|
cmd = "diff"
|
|
args.hists = []
|
|
_cmd_diff(args)
|
|
else:
|
|
cmd = args.cmd
|
|
args.func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_main()
|