Files
fedora-infra_ansible/files/scripts/updates-uptime-cmd.py
2025-08-13 18:27:35 -04:00

935 lines
27 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.
# 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 1d ... see what hasn't been rebooted in the last 24 hours.
# $0 uptime 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 glob
import locale
import shutil
import time
# If we try to update this seconds since the file changed, flush the
# ansible FACT cache.
conf_dur_flush_cache = (60*60*8)
# 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
# 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
# Remove suffix noise in names.
conf_suffix_dns_replace = {
'.fedoraproject.org' : '.<FP>.org',
'.fedorainfracloud.org' : '.<FIC>.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 = conf_path + "ansible-list-updates-uptime.txt"
backup_today = time.strftime("%Y-%m-%d", time.gmtime())
fname_today = fname + '.' + backup_today
backups = sorted(x.removeprefix(fname + '.') for x in glob.glob(fname + '.*'))
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_stat_4_hosts *= 4
conf_suffix_dns_replace = {}
_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']
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']
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 self.osname != other.osname:
return False
if self.osvers != other.osvers:
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 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", "info",
"list",
"stats",
"update", "update-fast", "update-flush",
"update-daily", "update-daily-refresh",
"uptime",):
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'
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():
global cmp
global cmp_arg
if len(sys.argv) < 2 or sys.argv[1] == "main":
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:
_usage()
print("Backups:", ", ".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 = "Unknown"
osvers = "?"
if ' ' in date:
date, osname, osvers = date.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, name):
for data in datas:
if not fnmatch.fnmatch(data.name, name):
continue
yield data
# Filter datas using uptime as a minium.
def filter_uptime_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()
_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
# Return stats for updates added/deleted between two data sets.
def _diffstats(data1, data2):
uadd, udel = 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
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)
continue
if d1 == d2:
data1.pop(0)
data2.pop(0)
continue
if d1.osinfo != d2.osinfo:
udel -= d1.rpms
uadd += d2.rpms
data1.pop(0)
data2.pop(0)
continue
# Now name is eq and osinfo is eq
# 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
def _ui_diffstats(data1, data2):
cmpds = _diffstats(data1, data2)
return _ui_int(cmpds[0]), _ui_int(cmpds[1])
# 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 <cmd>
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.
host [host*] [backup]
= See the current state of a host(s), can be filtered by name.
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
= Create the file.
update-flush
= Create the file, after flushing ansible caches.
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()
def _backup_suffix(backup):
suffix = ''
if backup == backup_today:
if ident:
suffix = ' (today, is identical)'
else:
suffix = ' (today)'
if backup == backup_yesterday:
suffix = ' (yesterday)'
return suffix
if cmd in ("backups", "hist", "history"):
ident = _backup_today_identical()
print("History:")
last_name = "main"
last_data = list(sorted(fname_datas()))
last_suff = ""
# 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") + 2
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))))
updates = _ui_int(sum(d.rpms for d in data))
hl = max(hl, len(_ui_int(len(data))))
ul = max(ul, len(updates))
print(" %10s %*s %*s %*s %*s" % ("Day", hl, "Hosts",
ul, "Updates", ul, "Avail", ul, "Inst."))
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' % (last_name,
hl, _ui_int(len(last_data)),
ul, updates, ul, cmpds[0], ul+1, cmpds[1], 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 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.
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": # Use ansible FACT cache for uptime.
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 --flush-cache")
# 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: Backup for today does not exist!", file=sys.stderr)
if fname_yesterday is None:
print(" Warning: Backup for yesterday does not exist!", file=sys.stderr)
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"), len(_ui_int(updates)))
_max_len_upts = len(_ui_dur(awake))
_max_len_date = 0
_max_update_correct(' ')
print("%-16s %6s %*s %*s" % ("OS", "Hosts",
_max_len_rpms, "Updates", _max_len_upts, "Uptime"))
print("-" * (16+2+6+1+_max_len_rpms+1+_max_len_upts))
print("%-16s: %6s %*s %*s" % ("All", _ui_int(len(data)),
_max_len_rpms, _ui_int(updates), _max_len_upts, _ui_dur(awake)))
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
print(" %-14s: %6s %*s %*s" % (osi, _ui_int(osdata['hosts'][osi]),
_max_len_rpms, _ui_int(osdata['updates'][osi]),
_max_len_upts, _ui_dur(osdata['uptimes'][osi])))
if '/' in osi:
print(" %s%-*s: %6s %*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])))
print("-" * (16+2+6+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(host, data):
hosts = []
for x in data:
if fnmatch.fnmatch(x.name, host):
hosts.append(x)
if not hosts:
print("Not host(s) matched:", host)
sys.exit(2)
for host in hosts:
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 cmd in ("host", "info"):
if cmd == "host":
host = "batcave*"
else:
host = "*"
if len(sys.argv) >= 2:
host = sys.argv.pop(1)
if len(sys.argv) >= 2 and sys.argv[1] == "all":
for b in backups:
print("Backup:", b)
_print_info(host, lines2datas(bfname2lines(b)))
sys.argv = [sys.argv[0]]
print("Main:")
_cmp_arg()
_print_info(host, fname1())
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 == "list":
host = "*"
if len(sys.argv) >= 2:
host = sys.argv.pop(1)
_cmp_arg()
data = fname1()
data = list(filter_name_datas(data, host))
_max_update(data)
_max_update_correct('')
for d1 in data:
_print_line('', d1)
_explain_ui_name()
if cmd == "uptime":
age = 0
if len(sys.argv) >= 2:
age = parse_duration(sys.argv.pop(1))
_cmp_arg()
data = fname1()
data = list(filter_uptime_datas(data, age))
_max_update(data)
_max_update_correct('')
for d1 in data:
_print_line('', d1)
_explain_ui_name()
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(' ')
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
if d1 == d2:
_print_line(' ', d2)
data1.pop(0)
data2.pop(0)
continue
if cmd == "diff-u":
_print_line('-', d1)
data1.pop(0)
_print_line('+', d2)
data2.pop(0)
continue
# diff
data1.pop(0)
_print_line('!', d2)
data2.pop(0)
continue
print('hosts=%s updates=%s (a=%s i=%s)' % (hosts, updates, cmpds[0],cmpds[1]))
_explain_ui_name()