#! /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. # Examples: ($0 = updates-uptime-cmd.py) # $0 update = create the file and/or do backups # $0 diff [x] [y] = see the difference between the current state and history # $0 uptime [x] = see the current state, can be filtered for uptime >= x # $0 info [x] = see the current state, in long form, can be filtered by name # $0 host [x] = see the current state of a host(s), can be filtered by name # $0 list [x] = see the current state, can be filtered by name # $0 history = see history # $0 history-keep = clenaup old history # $0 stats [x] = see stats, can specify a backup # $0 list '*.stg.*' ... see what staging looks like. # $0 list '*copr*' ... see what copr looks like. # $0 history-keep 4 ... keep four days of history (including today) # $0 uptime-min 1d ... see what hasn't been rebooted in the last 24 hours. # $0 uptime-max 1d ... see what has been rebooted in the last 24 hours. # $0 uptime-min 25w ... see what hasn't been rebooted in too damn long. # $0 update-daily-refresh ... daily update, including a new history, and # refresh the main file (so any old hosts aren't there anymore). import os import sys import fnmatch import locale import shutil import time # Use utf8 prefixes in diff conf_utf8_diff = 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 # 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 # 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 # 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' : '..org', '.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/" # 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') _fname = "ansible-list-updates-uptime.txt" fname = conf_path + _fname backup_today = time.strftime("%Y-%m-%d", time.gmtime()) fname_today = fname + '.' + backup_today # History files are named .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 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 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 = {} 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): if self.name > other.name: return True if self.name != other.name: 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) 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} 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): return format_duration(dur, short=conf_short_duration, 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 _backup_today_identical(): if fname_today is None: return False b = backup_today f1 = os.stat(fname) f2 = os.stat(fname + '.' + b) 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 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 _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 fnmatch.fnmatch(data.name, name): break else: continue yield data # Filter datas using uptime as a minium. def filter_uptime_min_datas(datas, uptime): for data in datas: if 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 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 -v 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))) def fname1(): if cmp_arg: return lines2datas(bfname2lines(cmp)) 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 = time.mktime(time.strptime(d1.date, "%Y-%m-%d")) tm2 = time.mktime(time.strptime(d2.date, "%Y-%m-%d")) 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 mw = _max_terminal_width - len(prefix) 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, data2) return _ui_int(cmpds[0]), _ui_int(cmpds[1]), _ui_int(cmpds[2]) # This is the real __main__ start ... def _usage(): prog = "updates+uptime" if sys.argv: prog = os.path.basename(sys.argv[0]) pl = " " * len(prog) print(""" Usage: %s Cmds: help = This message. 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 = Show history data. history-keep [days] = Cleanup old history. hosts [host*] [host*]... = See the history of a host(s). info [host*] [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. stats [backup] = Show stats. update = Create the file and/or do backups. update-fast update-flush = Create the file. update-daily = update-flush and do backups. update-daily-refresh = update-daily with new main file. uptime [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) 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" 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 _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 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 if cmd in ("backups", "hist", "history"): ident = _backup_today_identical() print("History:") last_name = "main" last_data = list(sorted(fname_datas())) last_suff = "" hl, ul, rl = _hist_lengths() print(" %10s %*s %*s %*s %*s %*s" % ("Day", hl, "Hosts", ul, "Updates", ul, "Avail", ul+1, "Inst.", rl, "Boots")) 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)) 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 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 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("%-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() 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) 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 _print_line(prefix, data): print("%s%-*s %*s %*s %*s %s" % (prefix, _max_len_name, _ui_name(data.name), _max_len_rpms, _ui_int(data.rpms), _max_len_upts, _ui_dur(data.uptime), _max_len_date, data.date, _ui_osinfo(data))) 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] _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() if cmd in ("uptime", "uptime-max", "uptime-min"): age = 0 if len(sys.argv) >= 2: age = parse_duration(sys.argv.pop(1)) _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) continue _print_line(' ', d2) continue # Something about host changed, show old/new... if show_both: _print_line('-', d1) _print_line('+', d2) continue # Something changed, but we only show the new data... if conf_utf8_diff: if False: pass elif not _wild_eq(d1.machine_id, d2.machine_id): _print_line(_conf_utf8_diff_hw, d2) elif not _wild_info_eq(d1, d2): _print_line(_conf_utf8_diff_os, d2) elif host_rebooted(d1, d2) and d1.rpms > d2.rpms: _print_line(_conf_utf8_boot_up, d2) elif host_rebooted(d1, d2): _print_line(_conf_utf8_boot_ed, d2) elif d1.rpms > d2.rpms: _print_line(_conf_utf8_less_up, d2) else: # d1.rpms < d2.rpms: _print_line(_conf_utf8_more_up, d2) continue _print_line('!', d2) continue 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") print('hosts=%s updates=%s (a=%s i=%s) boots=%s' % (hosts, updates, cmpds[0], cmpds[1], cmpds[2])) _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:] ident = _backup_today_identical() print("Hosts history:") last_name = "main" last_data = list(sorted(filter_name_datas(fname_datas(), 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(' ') done = False for backup in reversed(backups): data = filter_name_datas(lines2datas(bfname2lines(backup)), hosts) data = list(sorted(data)) 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()) 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])) 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()