From 5a6e1d88746b3efa31bb6d209423045a80be85d1 Mon Sep 17 00:00:00 2001 From: James Antill Date: Mon, 29 Sep 2025 18:38:39 -0400 Subject: [PATCH] updates-uptimes: Big freeze updates. Uses argparse now. Signed-off-by: James Antill --- files/scripts/updates-uptime-cmd.py | 1722 ++++++++++++++++++--------- 1 file changed, 1162 insertions(+), 560 deletions(-) diff --git a/files/scripts/updates-uptime-cmd.py b/files/scripts/updates-uptime-cmd.py index 07c0358a5f..8b13f60f7d 100755 --- a/files/scripts/updates-uptime-cmd.py +++ b/files/scripts/updates-uptime-cmd.py @@ -29,13 +29,14 @@ import os import sys +import argparse import fnmatch import locale import shutil import time -# Use utf8 prefixes in diff -conf_utf8_diff = True +# 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 = '➚' @@ -43,6 +44,32 @@ _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. @@ -54,6 +81,18 @@ 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 + # Do we use a shorter duration by default (drop minutes/seconds) conf_short_duration = True @@ -82,28 +121,146 @@ conf_suffix_dns_replace = { '.fedorainfracloud.org' : '..org', } _suffix_dns_replace = {} -for x in conf_suffix_dns_replace: - _suffix_dns_replace[x] = False # 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(" Error: 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 + # Have nice "plain" numbers... def _ui_int(num): - return locale.format_string('%d', int(num), grouping=True) -try: - locale.setlocale(locale.LC_ALL, '') -except locale.Error: - # default to C locale if we get a failure. - print(' Warning: Failed to set locale, defaulting to C', file=sys.stderr) - os.environ['LC_ALL'] = 'C' - locale.setlocale(locale.LC_ALL, 'C') + 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_ansi(text, codes, align=0): + olen = len(text) + if abs(align) > olen: # "%*s", align, text + extra = abs(align) - olen + if align > 0: + text = " " * extra + text + else: + text = text + " " * extra + if not conf_ansi_terminal or not codes or olen == 0: + return text -_fname = "ansible-list-updates-uptime.txt" -fname = conf_path + _fname -backup_today = time.strftime("%Y-%m-%d", time.gmtime()) -fname_today = fname + '.' + backup_today + 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) # History files are named .YYYY-MM-DD def _glob_hist_suffix(): @@ -124,28 +281,59 @@ def _glob_hist_suffix(): if fn[8] not in "0123": continue # Day if fn[9] not in "0123456789": continue yield fn -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 +_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 -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 + conf_path = os.path.expanduser(conf_path) -if len(sys.argv) >= 2: - if '-v' in sys.argv: - sys.argv.remove('-v') # In theory sys.argv[0] but meh - conf_small_osinfo = False - conf_short_duration = False - conf_info_machine_ids = True - conf_stat_4_hosts *= 4 - conf_suffix_dns_replace = {} + 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 >= 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'] *= 4 def _wild_eq(s1, s2): @@ -255,21 +443,6 @@ class Host(): return "%s/%s" % (self.osname_small, self.osvers) -cmd = "diff" -if len(sys.argv) >= 2: - if sys.argv[1] in ("backups", "backups-keep", - "hist", "history", "history-keep", - "diff", "diff-u", - "help", - "host", "hosts", - "info", "information", - "list", "list-n", - "stats", - "update", "update-fast", "update-flush", - "update-daily", "update-daily-refresh", - "uptime", "uptime-min", "uptime-max",): - cmd = sys.argv.pop(1) - _tm_d = {'d' : 60*60*24, 'h' : 60*60, 'm' : 60, 's' : 1, 'w' : 60*60*24*7, 'q' : 60*60*24*7*13} @@ -342,51 +515,26 @@ def _main_file_recent(): return False return True -def _backup_today_identical(): - if fname_today is None: - return False - - b = backup_today - f1 = os.stat(fname) - f2 = os.stat(fname + '.' + b) +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 -cmp_arg = False -cmp = None -# This does arguments for a bunch of commands, like stats/list/etc. -# by using fname1() after, which looks at cmp_arg. -# But also does diff arguments. -def _cmp_arg(usage=True): - global cmp - global cmp_arg +def _backup_yesterday_identical(): + if fname_yesterday is None: + return False - if len(sys.argv) < 2 or sys.argv[1] == "main": - cmp_arg = False - if len(sys.argv) >= 2: - sys.argv.pop(1) - cmp = backups[-1] # Most recent - if len(backups) > 1 and _backup_today_identical(): - # Eg. if you just do one update a day, you want to cmp vs. - # the previous day, not today. - cmp = backups[-2] - elif sys.argv[1] == "today" and fname_today is not None: - cmp = backup_today - cmp_arg = True - elif sys.argv[1] == "yesterday" and fname_yesterday is not None: - cmp = backup_yesterday - cmp_arg = True - elif sys.argv[1] not in backups: - if usage: - _usage() - print("History:", ", ".join(backups)) - sys.exit(1) - else: - cmp = sys.argv[1] - cmp_arg = True + 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 '.' @@ -427,6 +575,35 @@ def filter_name_datas(datas, names): 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 fnmatch.fnmatch(data.osinfo, name): + break + if fnmatch.fnmatch(data.osinfo_small, name): + break + if fnmatch.fnmatch(data.osname, name): + break + if fnmatch.fnmatch(data.osname_small, name): + break + if fnmatch.fnmatch(data.osvers, name): + break + off = data.osvers.find('.') + if off != -1: + vers = data.osvers[:off] + if fnmatch.fnmatch(vers, name): + break + + else: + continue + yield data + # Filter datas using uptime as a minium. def filter_uptime_min_datas(datas, uptime): for data in datas: @@ -469,7 +646,7 @@ def _explain_ui_name(): pre = " :" done = True if done: - print(" : Use -v to show full names.") + print(" : Use -vv to show full names.") def fname2lines(fname): return [x.strip() for x in open(fname).readlines()] @@ -492,9 +669,46 @@ def _maybe_dynamic_uptime(data): def fname_datas(): return _maybe_dynamic_uptime(lines2datas(fname2lines(fname))) -def fname1(): - if cmp_arg: - return lines2datas(bfname2lines(cmp)) + +# 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. @@ -565,70 +779,79 @@ def _wild_info_eq(d1, d2): # Return stats for updates added/deleted between two data sets. def _diffstats(data1, data2): - uadd, udel, boot = 0, 0, 0 + 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 + 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] + d1 = data1[0] + d2 = data2[0] - if d1.name < d2.name: - udel -= d1.rpms - data1.pop(0) - continue + 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.name > d2.name: + uadd += d2.rpms + data2.pop(0) + boot += 1 + continue - if d1 == d2: + 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) - 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 + # diffstat returns... + return uadd, udel, boot def _ui_diffstats(data1, data2): - cmpds = _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 ... @@ -639,6 +862,13 @@ def _usage(): pl = " " * len(prog) print(""" Usage: %s + Optional arguments: + -h, --help 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: help = This message. @@ -655,12 +885,20 @@ def _usage(): hosts [host*] [host*]... = See the history of a host(s). - info [host*] [backup] + hosts-u [host*] [host*]... + = See the history of a host(s), shows before/after. + info [host*] [backup] [backup]... = See the current state, in long form, can be filtered by name. list [host*] [backup] = See the current state, can be filtered by name. + list-n [host*] [host*]... + = See the current state, can be filtered by multiple names. + oslist [os*] [backup] + = See the current state, can be filtered by OS. + oslist-n [os*] [os*]... + = See the current state, can be filtered by multiple OSes. - stats [backup] + stats [backup] [host*] [host*]... = Show stats. update @@ -673,478 +911,842 @@ def _usage(): update-daily-refresh = update-daily with new main file. - uptime [duration] [backup] + 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. """ % (prog,)) -if cmd == "help": - _usage() -if cmd in ("backups-keep", "history-keep"): - keep = 8 - if len(sys.argv) >= 2: - keep = int(sys.argv.pop(1)) - if keep <= 0: - _usage() - sys.exit(1) - while keep < len(backups): - # We just keep the newest N - b = backups.pop(0) - print("Removing:", b) - fn = fname + '.' + b - os.unlink(fn) +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) -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" - else: - mtime = os.path.getmtime(fname) - if (int(time.time()) - mtime) > conf_dur_flush_cache: - cmd = "update-fast" +def _cmd_update(args): + 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-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") + 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") -# 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 _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 ident: - suffix = ' (today, is eq)' - else: - suffix = ' (today)' - if backup == backup_yesterday: - suffix = ' (yesterday)' - return suffix + 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 + # 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 -if cmd in ("backups", "hist", "history"): - ident = _backup_today_identical() - print("History:") - last_name = "main" - last_data = list(sorted(fname_datas())) - last_suff = "" +def _cmd_history(args): + global backups - hl, ul, rl = _hist_lengths() + last_name = "main" + last_data = list(sorted(fname_datas())) + last_suff = "" - print(" %10s %*s %*s %*s %*s %*s" % ("Day", hl, "Hosts", - ul, "Updates", ul, "Avail", ul+1, "Inst.", rl, "Boots")) + hl, ul, rl = _hist_lengths() - for backup in reversed(backups): - data = list(sorted(lines2datas(bfname2lines(backup)))) - updates = _ui_int(sum(d.rpms for d in last_data)) - ul = max(ul, len(updates)) - cmpds = _ui_diffstats(data.copy(), last_data.copy()) - print(' %10s %*s %*s, %*s %*s, %*s %s' % (last_name, - hl, _ui_int(len(last_data)), - ul, 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)) + 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)) -def _cli_match_host(data): - if len(sys.argv) >= 2: - host = sys.argv.pop(1) - print("Matching:", host) - data = filter_name_datas(data, [host]) - data = list(data) - if not data: - print("Not host(s) matched:", host) - sys.exit(2) - return data + if conf_hist_show > 0 and conf_hist_show < len(backups): + backups = backups[-conf_hist_show:] -if cmd == "stats": - _cmp_arg() - data = fname1() - if cmp_arg: - sys.argv.pop(1) - data = list(_cli_match_host(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 + for backup in reversed(backups): + data = list(sorted(lines2datas(bfname2lines(backup)))) + 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) - 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) + cmpds = _ui_diffstats(data, last_data) - # Tier 2/3 hosts... - if d2.osname not in osdata['hosts']: - osdata['hosts'][d2.osname] = 0 - osdata['hosts'][d2.osname] += 1 + 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)) - if d2.osinfo not in osdata['hosts']: - osdata['hosts'][d2.osinfo] = 0 - osdata['hosts'][d2.osinfo] += 1 +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 - 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 +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 - 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) + 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 - 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 + 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) - 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) + # Tier 2/3 hosts... + if d2.osname not in osdata['hosts']: + osdata['hosts'][d2.osname] = 0 + osdata['hosts'][d2.osname] += 1 - # 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("%-16s %6s %*s %*s %*s %*s" % ("OS", "Hosts", - _max_len_rpms, "Updates", _max_len_upts, "Uptime", - _max_len_rpms, "Updates/h", _max_len_upts, "Uptime/h")) - 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 = 12 - for osi in sorted(osdata['hosts']): - if '/' not in osi: - if len(osdata['vers'][osi]) == 1: - subprefix = '' - subplen = 14 - continue - subprefix = ' ' - subplen = 12 - nhosts = osdata['hosts'][osi] - print(" %-14s: %6s %*s %*s %*s %*s" % (osi, _ui_int(nhosts), - _max_len_rpms, _ui_int(osdata['updates'][osi]), - _max_len_upts, _ui_dur(osdata['uptimes'][osi]), - _max_len_rpms, _ui_int(osdata['updates'][osi] / nhosts), - _max_len_upts, _ui_dur(osdata['uptimes'][osi] / nhosts))) - if '/' in osi: - nhosts = osdata['hosts'][osi] - print(" %s%-*s: %6s %*s %*s %*s %*s" % (subprefix, subplen, osi, - _ui_int(osdata['hosts'][osi]), - _max_len_rpms, _ui_int(osdata['updates'][osi]), - _max_len_upts, _ui_dur(osdata['uptimes'][osi]), - _max_len_rpms, _ui_int(osdata['updates'][osi] / nhosts), - _max_len_upts, _ui_dur(osdata['uptimes'][osi] / nhosts))) - 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 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 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() + 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 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 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 fnmatch.fnmatch(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:", host.date) - if conf_info_machine_ids: - print(" Machine:", host.machine_id) - print(" Boot:", host.boot_id) + fhosts = [] + for x in data: + for host in hosts: + if not fnmatch.fnmatch(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:", host.date) + if conf_info_machine_ids: + print(" Machine:", host.machine_id) + print(" Boot:", host.boot_id) -if cmd in ("information", "info"): - hosts = conf_important_hosts.copy() - if len(sys.argv) >= 2: - hosts = [sys.argv.pop(1)] - if len(sys.argv) >= 2 and sys.argv[1] == "all": - for b in backups: - print("History:", b) - _print_info(hosts, lines2datas(bfname2lines(b))) - sys.argv = [sys.argv[0]] - while True: - _cmp_arg() - if cmp_arg: # One or more historical files... - print("History:", sys.argv.pop(1)) - else: - print("Main:") - _print_info(hosts, fname1()) - if len(sys.argv) < 2: - break +def _cmd_info(args): + 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))) + for hist in args.hists: + if hist != "main": # One or more historical files... + print("History:", hist) + else: + print("Main:") + _print_info(hosts, fname1([hist])) -def _print_line(prefix, data): - print("%s%-*s %*s %*s %*s %s" % (prefix, - _max_len_name, _ui_name(data.name), +# 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): + global _prnt_line_saved + 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), - _max_len_date, data.date, _ui_osinfo(data))) + _max_len_date, data.date, _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) +# -n variants match multiple things, but only allow looking at current data +def _cmd_list(args): + hosts = [] + osnames = [] + if hasattr(args, 'host'): # FIXME: argparse can't do this for us :( + args.hosts = [args.host] + if hasattr(args, 'osname'): + args.osnames = [args.osname] + if args.hosts: + hosts = args.hosts[:] + if args.osnames: + osnames = args.osnames[:] -if cmd in ("list", "list-n"): - hosts = [] - if len(sys.argv) >= 2: - if cmd == "list": - hosts = [sys.argv.pop(1)] - else: - hosts = sys.argv[1:] - sys.argv = sys.argv[:1] + data = fname1(args.hists[:]) + 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")) + for d1 in data: + _print_line('', d1) + _explain_ui_name() - _cmp_arg() - data = fname1() - data = list(filter_name_datas(data, hosts)) - _max_update(data) - _max_update_correct('') - for d1 in data: - _print_line('', d1) - _explain_ui_name() +def _cmd_uptime(args): + age = 0 + if args.dur > 0: + age = args.dur -if cmd in ("uptime", "uptime-max", "uptime-min"): - age = 0 - if len(sys.argv) >= 2: - age = parse_duration(sys.argv.pop(1)) + data = fname1(args.hists[:]) + if cmd == "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('') + for d1 in data: + _print_line('', d1) + _explain_ui_name() - _cmp_arg() - data = fname1() - if cmd == "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('') - for d1 in data: - _print_line('', d1) - _explain_ui_name() - -def _diff_hosts(data1, data2, show_both=False, show_utf8=True): - while len(data1) > 0 or len(data2) > 0: - if len(data1) <= 0: - _print_line('+', data2[0]) - data2.pop(0) - continue - if len(data2) <= 0: - _print_line('-', data1[0]) - data1.pop(0) - continue - - d1 = data1[0] - d2 = data2[0] - - if d1.name < d2.name: - _print_line('-', d1) - data1.pop(0) - continue - - if d1.name > d2.name: - _print_line('+', d2) - 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_diff and host_rebooted(d1, d2): - _print_line(_conf_utf8_boot_ed, d2) +def _diff_hosts(data1, data2, show_both=False, show_utf8=True, skip_eq=False): + while len(data1) > 0 or len(data2) > 0: + if len(data1) <= 0: + _print_line('+', data2[0]) + data2.pop(0) + continue + if len(data2) <= 0: + _print_line('-', data1[0]) + data1.pop(0) continue - _print_line(' ', d2) - continue - # Something about host changed, show old/new... - if show_both: - _print_line('-', d1) - _print_line('+', d2) - continue + d1 = data1[0] + d2 = data2[0] - # Something changed, but we only show the new data... - if conf_utf8_diff: + if d1.name < d2.name: + _print_line('-', d1) + data1.pop(0) + continue + + if d1.name > d2.name: + _print_line('+', d2) + 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) + continue + _print_line(' ', d2, save=skip_eq) + continue + + # Something changed, see what and set utf8 prefix and highlight if False: pass elif not _wild_eq(d1.machine_id, d2.machine_id): - _print_line(_conf_utf8_diff_hw, d2) + utf8, high = _conf_utf8_diff_hw, conf_term_host_diff_hw elif not _wild_info_eq(d1, d2): - _print_line(_conf_utf8_diff_os, d2) + utf8, high = _conf_utf8_diff_os, conf_term_host_diff_os elif host_rebooted(d1, d2) and d1.rpms > d2.rpms: - _print_line(_conf_utf8_boot_up, d2) + utf8, high = _conf_utf8_boot_up, conf_term_host_boot_up elif host_rebooted(d1, d2): - _print_line(_conf_utf8_boot_ed, d2) + utf8, high = _conf_utf8_boot_ed, conf_term_host_boot_ed elif d1.rpms > d2.rpms: - _print_line(_conf_utf8_less_up, d2) + utf8, high = _conf_utf8_less_up, conf_term_host_less_up else: # d1.rpms < d2.rpms: - _print_line(_conf_utf8_more_up, d2) + 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) + _print_line(utf8, d2, high=high) + continue + + # Something changed, but we only show the new data... + if not conf_utf8: + utf8 = '!' + _print_line(utf8, d2, high=high) + continue - _print_line('!', d2) - continue +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)) + 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(' ') -if cmd in ("diff", "diff-u"): - _cmp_arg() - fn1 = fname + '.' + cmp - fn2 = fname - data1 = fname2lines(fn1) - if len(sys.argv) >= 3: - # Doing a diff. between two backups... - if sys.argv[2] == 'today' and fname_today is not None: - fn2 = fname_today - if sys.argv[2] == 'yesterday' and fname_yesterday is not None: - fn2 = fname_yesterday - if sys.argv[2] in backups: - fn2 = fname + '.' + sys.argv[2] - data2 = fname2lines(fn2) - print("diff %s %s" % (fn1, fn2), file=sys.stderr) - data1 = list(sorted(lines2datas(data1))) - data2 = list(sorted(lines2datas(data2))) - if fn2 == fname: - data2 = _maybe_dynamic_uptime(data2) - hosts = _ui_int(len(data2)) - updates = _ui_int(sum(d.rpms for d in data2)) - ul = len(updates) - cmpds = _ui_diffstats(data1.copy(), data2.copy()) - _max_update(data1) - _max_update(data2) - _max_update_correct(' ') + _diff_hosts(data1, data2, show_both=cmd == "diff-u", show_utf8=cmd == "diff") - _diff_hosts(data1, data2, show_both=cmd == "diff-u", show_utf8=cmd == "diff") - - print('hosts=%s updates=%s (a=%s i=%s) boots=%s' % (hosts, updates, - cmpds[0], cmpds[1], cmpds[2])) - _explain_ui_name() + _print_diffstats(hosts, updates, cmpds) + _explain_ui_name() # Like diff/history mixed, but for specific hosts... -if cmd in ("hosts", "host", "hosts-u", "host-u"): - hosts = conf_important_hosts.copy() - if len(sys.argv) >= 2: - hosts = sys.argv[1:] +def _cmd_host(args): + global conf_host_skip_eq - ident = _backup_today_identical() - print("Hosts history:") - last_name = "main" - last_data = list(sorted(filter_name_datas(fname_datas(), hosts))) + hosts = conf_important_hosts.copy() + if args.hosts: + hosts = args.hosts - _max_update(last_data) - if not conf_fast_width_history: - for backup in reversed(backups): - data = filter_name_datas(lines2datas(bfname2lines(backup)), hosts) - data = list(sorted(data)) - _max_update(data) - _max_update_correct(' ') + print("Hosts history:") + last_name = "main" + last_data = list(sorted(filter_name_datas(fname_datas(), hosts))) - done = False - for backup in reversed(backups): - data = filter_name_datas(lines2datas(bfname2lines(backup)), hosts) - data = list(sorted(data)) + _max_update(last_data) + if not conf_fast_width_history: + for backup in reversed(backups): + data = filter_name_datas(lines2datas(bfname2lines(backup)), hosts) + data = list(sorted(data)) + _max_update(data) + _max_update_correct(' ') - if done: - print("") - done = True - print("Host diff: %s %s" % (backup, last_name), file=sys.stderr) - if not conf_fast_width_history: - _max_update(data) - _max_update_correct(' ') - _diff_hosts(data.copy(), last_data.copy(), show_both=cmd.endswith("-u")) - if False: - cmpds = _ui_diffstats(data.copy(), last_data.copy()) + if conf_utf8: + sep = conf_host_arrow_utf8 + else: + sep = conf_host_arrow_asci + done = False + skipped_num = 0 + thostnum = 0 + for backup in reversed(backups): + data = filter_name_datas(lines2datas(bfname2lines(backup)), hosts) + data = list(sorted(data)) + + 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) - updates = _ui_int(sum(d.rpms for d in last_data)) - print('hosts=%s updates=%s (a=%s i=%s) boots=%s' % (hostnum, updates, - cmpds[0], cmpds[1], cmpds[2])) + 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 - last_name = backup - last_data = data - if done: - print("") - print("Host data: %s" % (last_name,), file=sys.stderr) - for d1 in last_data: - _print_line(' ', d1) - _explain_ui_name() + 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) + for d1 in last_data: + _print_line(' ', d1) + _explain_ui_name() + + +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): + 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) + +def _main(): + global conf_ansi_terminal + global conf_path + global cmd + + _user_conf() + + parser = argparse.ArgumentParser() + 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) + + # We do this here so that `$0 -v stats -v` works. + margs, args = parser.parse_known_args() + + subparsers = parser.add_subparsers(dest="cmd") + + cmd = subparsers.add_parser("help") + cmd.set_defaults(func=lambda x: _usage()) # parser.print_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))) + + 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... + + # diff/diff-u commands + cmd = subparsers.add_parser("diff", aliases=['diff-u'], help="diff") + cmd.add_argument("hists", nargs='*', type=_cmdline_arg_hist, help="history file") + __defs(func=_cmd_diff) + + # hosts/hosts-u commands + als = ['host', 'hosts-u', 'host-u'] + hlp = "show history data about specific hosts" + cmd = subparsers.add_parser("hosts", aliases=als, help=hlp) + cmd.add_argument("hosts", nargs='*', help="wildcard hostname(s)") + __defs(func=_cmd_host) + + # history command + cmd = subparsers.add_parser("history", aliases=['hist'], help="show history") + __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("num", type=_cmdline_arg_positive_integer, help=hlp) + __defs(func=_cmd_history_keep) + + # info command + hlp = "show host information" + cmd = subparsers.add_parser("information", aliases=['info'], 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/oslist/oslist-n commands + cmd = subparsers.add_parser("list", help="list hosts") + cmd.add_argument("host", nargs='?', help="wildcard hostname") + cmd.add_argument("hists", nargs='*', type=_cmdline_arg_hist, help="history file") + cmd.set_defaults(osnames=[]) + __defs(func=_cmd_list) + + cmd = subparsers.add_parser("list-n", help="list hosts") + cmd.add_argument("hosts", nargs='*', help="wildcard hostname(s)") + cmd.set_defaults(osnames=[]) + __defs(func=_cmd_list) + + cmd = subparsers.add_parser("oslist", help="list hosts") + cmd.add_argument("osname", nargs='?', help="wildcard OSname") + cmd.add_argument("hists", nargs='*', type=_cmdline_arg_hist, help="history file") + cmd.set_defaults(hosts=[]) + __defs(func=_cmd_list) + + cmd = subparsers.add_parser("oslist-n", help="list hosts") + cmd.add_argument("osnames", nargs='*', help="wildcard OSname(s)") + cmd.set_defaults(hosts=[]) + __defs(func=_cmd_list) + + # stats commands + hlp = "show stats information" + cmd = subparsers.add_parser("statistics", aliases=['stats'], help=hlp) + cmd.add_argument("hist", nargs='?', type=_cmdline_arg_hist, default="main", + help="history file") + cmd.add_argument("hosts", nargs='*', help="wildcard hostname(s)") + __defs(func=_cmd_stats) + + # update commands + cmd = subparsers.add_parser("history", aliases=['hist'], help="show history") + __defs(func=_cmd_history) + + # uptime/uptime-min/uptime-max commands + cmd = subparsers.add_parser("uptime-max", help="list hosts") + cmd.add_argument("dur", type=_cmdline_arg_duration, + help="uptime maximum duration") + __defs(func=_cmd_uptime) + + als = ['uptime'] + 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()