#!/usr/bin/env python3 import htcondor import classad import sys import os import re import datetime import optparse from operator import itemgetter from time import sleep from copy import deepcopy ### command-line options ### parser = optparse.OptionParser(usage=( 'Usage: %prog [options]\n' ' %prog [options] \n\n' 'Only -c, -s, and --attrs options considered when passing ClassAds via files.')) # daemons parser.set_defaults(daemon=False) group = optparse.OptionGroup(parser, title='Daemon Options', description=( 'If none of these options are used, the local DAEMON_LIST will be searched ' 'in the order: SCHEDD, STARTD, COLLECTOR, NEGOTIATOR, MASTER. ' 'If -n NAME is not specified, the first ClassAd matching the constraint ' '"Machine == $(FULL_HOSTNAME)" (for Schedd/Startd/Master ads) ' 'or "Machine == $(COLLECTOR_HOST)" (for Collector/Negotiator ads) ' 'will be monitored.')) group.add_option('--collector', action='store_const', const='Collector', dest='daemon', help=( 'Monitor condor_collector ClassAds.')) group.add_option('--negotiator', action='store_const', const='Negotiator', dest='daemon', help=( 'Monitor condor_negotiator ClassAds.')) group.add_option('--master', action='store_const', const='Master', dest='daemon', help=( 'Monitor condor_master ClassAds.')) group.add_option('--schedd', action='store_const', const='Schedd', dest='daemon', help=( 'Monitor condor_schedd ClassAds.')) group.add_option('--startd', action='store_const', const='Startd', dest='daemon', help=( 'Monitor condor_startd ClassAds.')) parser.add_option_group(group) # live mode parser.add_option('-l', '--live', action='store_const', const=True, dest='live', default=False, help=( 'Display results in a live, top-like mode.')) # pool parser.add_option('-p', '--pool', dest='pool', default=None, help=( 'Query the specified Central Manager instead of $(COLLECTOR_HOST). ' 'Use of this option implies --collector unless otherwise specified.')) # name parser.add_option('-n', '--name', dest='name', default=None, help=( 'Query using the constraint Name == NAME. If omitted ' 'and multiple ClassAds are returned, only the daemon named ' 'in the first ClassAd returned will be monitored.')) # delay parser.add_option('-d', '--delay', type='int', dest='delay', default=htcondor.param['STATISTICS_WINDOW_QUANTUM'], help=( 'Specifies the delay between ClassAd updates, in integer seconds. ' 'If omitted, the value of the configuration variable ' 'STATISTICS_WINDOW_QUANTUM is used.')) # display columns parser.add_option('-c', '--columns', dest='column_set', default='default', help=( 'Choose the set of columns displayed. Valid column sets are: ' 'default, runtime, count, all.')) # sort column parser.add_option('-s', '--sort', dest='column', default='', help=( 'Sort table by column name. See the man page for a list of column names ' 'and definitions.')) # additional ClassAd attributes parser.add_option('--attrs', dest='attrs', default='', help=( 'Comma-delimited list of additional ClassAd attributes to monitor. ' 'Values will be considered as "counts".')) # set global variables based on command-line options opts, args = parser.parse_args() FILES = args DAEMON = opts.daemon LIVE = opts.live POOL = opts.pool NAME = opts.name DELAY = opts.delay COL_SET = opts.column_set.lower() SORT_COL = opts.column ATTRIBUTES = [attr.strip() for attr in opts.attrs.split(',')] # set default daemon from checking the DAEMON_LIST in specified order if not DAEMON: if POOL is not None: DAEMON = 'Collector' else: for DAEMON in ['Schedd', 'Startd', 'Collector', 'Negotiator', 'Master']: if DAEMON.lower() in htcondor.param['DAEMON_LIST'].lower(): break # set machine name if name is not set MACHINE = None if (NAME is None) and (POOL is None): if DAEMON in ['Schedd', 'Startd', 'Master']: MACHINE = htcondor.param['FULL_HOSTNAME'] else: MACHINE = htcondor.param['COLLECTOR_HOST'] # determine if we should do direct queries DIRECT = DAEMON in ['Schedd', 'Startd'] # column names COL_NAMES = [ 'TotalRt', # 0. Total Runtime 'InstRt', # 1. Runtime between ads 'InstAvg', # 2. Runtime/Count between ads 'TotAvg', # 3. Mean Runtime/Count since daemon start 'TotMax', # 4. Max Runtime/Count since daemon start 'TotMin', # 5. Min Runtime/Count since daemon start 'RtPctAvg', # 6. InstAvg / TotAvg ("percent of mean runtime") 'RtPctMax', # 7. InstAvg-TotMin / TotMax-TotMin ("percent of max runtime") 'RtSigmas', # 8. InstAvg-TotAvg / TotStd ("std devs from mean runtime") 'TotalCt', # 9. Total Count 'InstCt', # 10. Count between ads 'InstRate', # 11. Count/time between ads 'AvgRate', # 12. Count/time since daemon start 'CtPctAvg', # 13. InstRate / AvgRate ("percent of mean count rate") 'Item' # 14. Attribute name ] # column sets COL_SETS = { 'all': COL_NAMES, 'default': ['InstRt', 'InstAvg', 'TotAvg', 'TotMax', 'RtPctAvg', 'InstRate', 'AvgRate', 'Item'], 'runtime': ['InstRt', 'InstAvg', 'TotAvg', 'TotMax', 'RtPctAvg', 'RtPctMax', 'RtSigmas', 'Item'], 'count': ['TotalCt', 'InstCt', 'InstRate', 'AvgRate', 'CtPctAvg', 'Item'], } # use the default column set if not specified if not (COL_SET in COL_SETS): COL_SET = 'default' # check that SORT_COL is a valid column if not (SORT_COL in COL_NAMES): for col_name in COL_NAMES: if SORT_COL.lower() == col_name.lower(): SORT_COL = col_name break else: # if not a valid column, use the default if COL_SET != 'count': SORT_COL = 'InstRt' else: SORT_COL = 'InstRate' # check if output being piped PIPE = not sys.stdout.isatty() # live mode does not apply when piped or using ClassAd files if PIPE or FILES: LIVE = False ### ### ClassAd querying functions ### def getConstraint(machine = None, name = None): """Figure out what the constraint string should be and return it""" constraints = [] if machine is not None : constraints.append('(Machine =?= {0})'.format(classad.quote(machine))) if name is not None: constraints.append('(Name =?= {0})'.format(classad.quote(name))) constraint = ' && '.join(constraints) return constraint def warnMultipleAds(n, daemon, pool): """Prints a warning to stderr if the Collector returns multiple ads""" if pool is None: pool = htcondor.param['COLLECTOR_HOST'] sys.stderr.write(( 'Warning: Received {0} {1} ads from Collector {2}\n' 'Consider specifying a daemon name with -n NAME.\n' ).format(n, daemon, pool)) def errNoAds(daemon, pool): """Prints an error to stderr and quits if the Collector returns no ClassAds""" if pool is None: pool = htcondor.param['COLLECTOR_HOST'] sys.stderr.write(( 'Error: Did not receive any {0} ads from Collector {1}\n' ).format(daemon, pool)) sys.exit(1) def getDaemonName(pool, daemon, machine): """Returns Name attribute from the first ClassAd returned by the Collector""" # first try getting ads by constraining the Machine name collector = htcondor.Collector(pool) constraint = getConstraint(machine = machine) ads = collector.query(htcondor.AdTypes.names[daemon], constraint=constraint, projection=['Name']) # if that doesn't work, remove the constraint and try again if len(ads) == 0: ads = collector.query(htcondor.AdTypes.names[daemon], projection=['Name']) if len(ads) == 0: errNoAds(daemon, pool) if len(ads) > 1: warnMultipleAds(len(ads), daemon, pool) name = ads[0]['Name'] return name def queryClassAd(pool, daemon, name, direct = False, statistics = 'All:2'): """Returns the first ClassAd returned by the Collector""" collector = htcondor.Collector(pool) if direct: # direct queries take different arguments and return one ad kwargs = { 'daemon_type': htcondor.DaemonTypes.names[daemon], 'name': name, 'statistics': statistics } ad = collector.directQuery(**kwargs) else: # indirect queries may return multiple ads kwargs = { 'ad_type': htcondor.AdTypes.names[daemon], 'constraint': getConstraint(name = name), 'statistics': statistics } ads = collector.query(**kwargs) if len(ads) == 0: errNoAds(daemon, pool) if len(ads) > 1: warnMultipleAds(len(ads), daemon, pool) # return only the first ad ad = ads[0] return ad ### ### ClassAd parsing ### def getRuntimeAttrs(ad): """Returns a list of runtime attributes""" re_runtime = re.compile('^(.*)Runtime$') # some attributes should always be ignored re_ignore = re.compile('^DC(Socket|Pipe)') ignored_attrs = ['SCGetAutoCluster_cchit'] attrs = [] for key in ad.keys(): match = re_runtime.match(key) if match: attr = match.groups()[0] if not (re_ignore.match(attr) or (attr in ignored_attrs)): attrs.append(attr) return attrs ### calculations ### def computeUpdateTimes(old, new): """Returns the ClassAds' update times (Unix epoch)""" if (('DaemonStartTime' in new) and ('StatsLifetime' in new) and ('DaemonStartTime' in old) and ('StatsLifetime' in old)): t_0 = new['DaemonStartTime'] if t_0 != old['DaemonStartTime']: sys.stderr.write('Error: Daemon ads have different start times.\n') sys.exit(1) t_old = t_0 + old['StatsLifetime'] t_new = t_0 + new['StatsLifetime'] else: t_old = old['MyCurrentTime'] t_new = new['MyCurrentTime'] # check for same timestamp if (t_new - t_old) == 0: if new['MyCurrentTime'] > old['MyCurrentTime']: # try using MyCurrentTime t_new = new['MyCurrentTime'] t_old = old['MyCurrentTime'] else: # quit and notify if the ads have the same timestamp sys.stderr.write('Error: ClassAds have the same timestamp.\n') sys.exit(1) return (t_old, t_new) def computeInstDutyCycle(old, new): """Returns the daemon core duty cycle computed between two ClassAds""" if not (('DCSelectWaittime' in new) and ('DCPumpCycleSum' in new)): return None dPumpCycleCount = new['DCPumpCycleCount'] - old['DCPumpCycleCount'] dPumpCycleSum = new['DCPumpCycleSum'] - old['DCPumpCycleSum'] # check if a cycle has occured between ads if (dPumpCycleCount == 0) or (dPumpCycleSum < 1e-6): sys.stderr.write(('Duty cycle undefined, try setting a larger delay (-d).\n')) return None dSelectWaittime = new['DCSelectWaittime'] - old['DCSelectWaittime'] duty_cycle = 100*(1 - dSelectWaittime/float(dPumpCycleSum)) return duty_cycle def computeInstOpsPerSec(old, new): """Returns the daemon core commands per second computed between two ClassAds""" if not ('DCCommands' in new): return None t_old, t_new = computeUpdateTimes(old, new) dt = t_new - t_old dCommands = new['DCCommands'] - old['DCCommands'] ops_per_sec = dCommands / float(dt) return ops_per_sec def computeRuntimeStats(old, new): """Returns a list of derived runtime and count statistics computed between two ClassAds for each runtime attribute""" t_old, t_new = computeUpdateTimes(old, new) dt = t_new - t_old # StatsLifetime is not reported by Startd, so we can't compute # lifetime stats (e.g. average counts/sec) from Startd ads. if 'StatsLifetime' in new: t = new['StatsLifetime'] else: t = None # get the list of runtime attributes attrs = getRuntimeAttrs(new) # add any additional attributes specified by --attr for attr in ATTRIBUTES: attrs.append(attr) # build a list with tuples containing values of each statistic table = [] for attr in attrs: # check that attribute counts exist in both ads if (attr in new) and (attr in old): C = new[attr] dC = new[attr] - old[attr] else: # don't bother keeping this attribute if there are no counts continue # check that attribute runtimes exist in both ads if ((attr + 'Runtime') in new) and ((attr + 'Runtime') in old): R = new[attr + 'Runtime'] dR = new[attr + 'Runtime'] - old[attr + 'Runtime'] else: R, dR = (None, None) # compute runtime/count between ads if (dC > 0) and (dR is not None): R_curr = dR / float(dC) else: R_curr = None # grab the attribute's lifetime runtime stats R_ = {} for stat in ['Avg', 'Max', 'Min', 'Std']: if (attr + 'Runtime' + stat) in new: R_[stat] = new[attr + 'Runtime' + stat] else: R_[stat] = None R_pct_avg, R_pct_max, R_sigmas = (None, None, None) if R_curr is not None: # compare current runtime/count to lifetime runtime/count if R_['Avg'] is not None: R_pct_avg = 100. * R_curr / float(R_['Avg']) # compare current runtime/count to lifetime max/min range if (R_['Max'] is not None) and (R_['Min'] is not None): if R_['Max'] == R_['Min']: R_pct_max = 100. else: R_pct_max = 100. * ( (R_curr - R_['Min']) / float(R_['Max'] - R_['Min']) ) # compare difference between current and lifetime runtime/count # to the lifetime standard deviation in runtime/count if (R_['Avg'] is not None) and (R_['Std'] is not None): R_sigmas = (R_curr - R_['Avg']) / float(R_['Std']) # compute count/sec between ads C_curr = dC / float(dt) # compute lifetime counts/sec if t: C_avg = C / float(t) else: C_avg = None # compare current counts/sec to lifetime counts/sec C_pct_avg = None if (dC > 0) and (C_avg is not None): C_pct_avg = 100. * C_curr / float(C_avg) # cleanup item name if attr[0:2] == 'DC': attr = attr[2:] # store stats in a list in the same order as COL_NAMES row = [None]*len(COL_NAMES) row[COL_NAMES.index('Item')] = attr row[COL_NAMES.index('TotalRt')] = R row[COL_NAMES.index('InstRt')] = dR row[COL_NAMES.index('InstAvg')] = R_curr row[COL_NAMES.index('TotAvg')] = R_['Avg'] row[COL_NAMES.index('TotMax')] = R_['Max'] row[COL_NAMES.index('TotMin')] = R_['Min'] row[COL_NAMES.index('RtPctAvg')] = R_pct_avg row[COL_NAMES.index('RtPctMax')] = R_pct_max row[COL_NAMES.index('RtSigmas')] = R_sigmas row[COL_NAMES.index('TotalCt')] = C row[COL_NAMES.index('InstCt')] = dC row[COL_NAMES.index('InstRate')] = C_curr row[COL_NAMES.index('AvgRate')] = C_avg row[COL_NAMES.index('CtPctAvg')] = C_pct_avg # tupleize the stats list and add them to the larger list table.append(tuple(row)) return table ### view ### def getTerminalSize(): """Returns the height (lines) and width (characters) of the terminal""" try: # works on Linux height, width = [int(x) for x in os.popen('stty size', 'r').read().split()] width -= 1 # leave a blank column on the right except Exception: # otherwise assume standard console size height = 25 width = 80 - 1 # leave a blank column on the right return (height, width) def getRuntimeColFmts(col_name, table, fmt_type): """Returns the text and number format that best fits a column""" # the only string-type data displayed is at the end of the row # so it doesn't require any special formatting column_fmt = '{0}' number_fmt = '{0}' if fmt_type == 'str': return (column_fmt, number_fmt) # for numerical data, compare the length of the column name # to the length of the largest (in characters) number. name_width = len(col_name) # the "largest" number could be negative, so check both max and min i = COL_NAMES.index(col_name) max_val = max(table, key=lambda row: row[i] if row[i] is not None else 0)[i] min_val = min(table, key=lambda row: row[i] if row[i] is not None else 0)[i] try: max_width = len('{0:d}'.format(int(max_val))) except TypeError: max_width = 0 try: min_width = len('{0:d}'.format(int(min_val))) except TypeError: min_width = 0 n_width = max([max_width, min_width]) # for floats, we want the largest number of decimal places # that we can get away with without needlessly expanding the column size if fmt_type in ['float', 'floatneg']: # add one to the length of columns that could have negative numbers if fmt_type == 'floatneg': n_width += 1 # compare against max int length + 1 because of decimal point if (n_width + 1) < name_width: decimals = name_width - (n_width + 1) n_width = (n_width + decimals + 1) else: decimals = 0 col_width = max([n_width, name_width]) column_fmt = '{0:>%ds}' % (col_width,) number_fmt = '{0:>%d.%df}' % (col_width, decimals) # for integers, we just need to make sure that we aren't needlessly # expanding the column size unless the numbers are really large if fmt_type == 'int': col_width = max([n_width, name_width]) column_fmt = '{0:>%ds}' % (col_width,) number_fmt = '{0:>%dd}' % (col_width,) return (column_fmt, number_fmt) def formatMemorySize(size): """Returns a string of human-readable memory size""" # ClassAds report memory in KB, so start with K suffixes = ['K', 'M', 'G', 'T', 'P'] for i in range(len(suffixes)): if size/(10**(i*3)) < 1000: break size = size/float(10**(i*3)) suffix = suffixes[i] return '{0:.1f} {1}'.format(size, suffix) def printMonitorSelf(daemon, name, ad): """Prints the MonitorSelf stats and returns the number of lines printed""" out = [] t = ad['MonitorSelfTime'] out.append('{0} {1} status at {2}:'.format(daemon, name, datetime.datetime.fromtimestamp(t))) out.append('\n') if 'MonitorSelfAge' in ad: uptime = datetime.timedelta(seconds = ad['MonitorSelfAge']) out.append('Uptime: {0}'.format(uptime)) out.append('\n') if 'MonitorSelfSysCpuTime' in ad: sys_cpu_time = datetime.timedelta(seconds = ad['MonitorSelfSysCpuTime']) out.append('SysCpuTime: {0}'.format(sys_cpu_time)) out.append(' '*4) if 'MonitorSelfUserCpuTime' in ad: user_cpu_time = datetime.timedelta(seconds=ad['MonitorSelfUserCpuTime']) out.append('UserCpuTime: {0}'.format(user_cpu_time)) out.append('\n') if 'MonitorSelfImageSize' in ad: memory = formatMemorySize(ad['MonitorSelfImageSize']) out.append('Memory: {0}'.format(memory)) out.append(' '*4) if 'MonitorSelfResidentSetSize' in ad: rss = formatMemorySize(ad['MonitorSelfResidentSetSize']) out.append('RSS: {0}'.format(rss)) out.append('\n') out.append('\n') sys.stdout.write(''.join(out)) lines = out.count('\n') return lines def printRecentHealthStats(ad): """Prints the "recent" DC duty cycle and commands per second and returns the number of lines printed""" lines = 0 # not all ClassAds report the RecentStatsTickTime, # but MonitorSelfTime is a close approximation if 'RecentStatsTickTime' in ad: t = datetime.datetime.fromtimestamp(ad['RecentStatsTickTime']) else: t = datetime.datetime.fromtimestamp(ad['MonitorSelfTime']) sys.stdout.write('Recent DC status at {0}:\n'.format(t)) lines += 1 if 'RecentDaemonCoreDutyCycle' in ad: duty_cycle = 100*ad['RecentDaemonCoreDutyCycle'] if 'RecentStatsLifetime' in ad: duration = ad['RecentStatsLifetime'] sys.stdout.write('Duty Cycle (last {0}s): {1:.2f}% '.format( duration, duty_cycle)) else: sys.stdout.write('Duty Cycle: {0:.2f}% '.format(duty_cycle)) for attr in ['DCCommandsPerSecond_' + l for l in ['4m', '20m', '4h']]: if attr in ad: ops_per_sec = ad[attr] duration = attr.split('_')[1] sys.stdout.write('Ops/second (last {0}): {1:.3f}'.format( duration, ops_per_sec)) break sys.stdout.write('\n\n') lines += 2 return lines def printInstHealthStats(old, new): """Prints the DC duty cycle and commands per second between two ads and returns the number of lines printed""" duty_cycle = computeInstDutyCycle(old, new) ops_per_sec = computeInstOpsPerSec(old, new) # in the case that we cannot get the values from between ads, # just print the recent DC stats if (duty_cycle is None) or (ops_per_sec is None): return printRecentHealthStats(new) lines = 0 # print the time between ads t_old, t_new = computeUpdateTimes(old, new) t_old, t_new = [datetime.datetime.fromtimestamp(t) for t in [t_old, t_new]] sys.stdout.write('DC status from {0} to {1}:\n'.format(t_old, t_new)) lines += 1 # print the stats sys.stdout.write('Duty Cycle: {0:.2f}% '.format(duty_cycle)) sys.stdout.write('Ops/second: {0:.3f}\n'.format(ops_per_sec)) lines += 1 sys.stdout.write('\n') lines += 1 return lines def printRuntimeTable(old, new, col_set = COL_SET, sort_col = SORT_COL, lines = 0): """Prints the table of runtime statistics""" # check the terminal size height, width = getTerminalSize() # get the table of stats table = computeRuntimeStats(old, new) # get the text and number formats for each column col_fmts = {} for col in ['TotalCt', 'InstCt']: col_fmts[col] = getRuntimeColFmts(col, table, 'int') for col in ['RtSigmas']: col_fmts[col] = getRuntimeColFmts(col, table, 'floatneg') for col in ['Item']: col_fmts[col] = getRuntimeColFmts(col, table, 'str') for col in COL_NAMES: if not (col in col_fmts): col_fmts[col] = getRuntimeColFmts(col, table, 'float') # sort the table table.sort(key = itemgetter(COL_NAMES.index(sort_col)), reverse = True) # get the column names to be printed col_names = COL_SETS[col_set] # print the time between ads t_old, t_new = computeUpdateTimes(old, new) t_old, t_new = [datetime.datetime.fromtimestamp(t) for t in [t_old, t_new]] sys.stdout.write('Runtime stats from {0} to {1}:\n'.format(t_old, t_new)) lines += 1 # print the header cols = [] for col_name in col_names: cols.append(col_fmts[col_name][0].format(col_name)) col_string = ' '.join(cols) # crop the header to the terminal window if needed if (not PIPE) and (len(col_string) > width): col_string = col_string[:width] sys.stdout.write(col_string + '\n') lines += 1 # print the rows for row in table: # make a list of columns cols = [] # go in the order of col_names for col_name in col_names: col_value = row[COL_NAMES.index(col_name)] # if the value can't be formatted, it's probably None ("n/a") try: cols.append(col_fmts[col_name][1].format(col_value)) except (ValueError, TypeError): cols.append(col_fmts[col_name][0].format('n/a')) # merge the column list into a single string row_string = ' '.join(cols) # crop the row to the terminal window if needed if (not PIPE) and (len(row_string) > width): row_string = row_string[:width] # print the row sys.stdout.write(row_string + '\n') lines += 1 # leave a blank row at the bottom of the table if (not PIPE) and (lines >= height - 1): break def printCountdown(delay = DELAY): """Prints countdown to the next update to stderr""" now = datetime.datetime.utcnow() refresh_time = now + datetime.timedelta(seconds=delay) time_left = refresh_time - now while time_left > datetime.timedelta(seconds=0): # rewind the line (\r) and print countdown each second sys.stderr.write('\rUpdating ClassAd in {0:>5d} s'.format( time_left.seconds)) sys.stderr.flush() sleep(1) time_left = refresh_time - datetime.datetime.utcnow() sys.stderr.write('\rUpdating ClassAd ' + 10*'.' + '\n\n') sys.stderr.flush() def resetTerminal(): """Clears the current terminal output""" sys.stdout.write('\n\n') # add a few blank lines sys.stdout.flush() if os.name == 'nt': os.system('cls') else: os.system('clear') if __name__ == '__main__': # Two modes of input: # 1. ClassAds parsed from two files # 2. ClassAds queried from the Collector # Two modes of output: # 1. stdout to the terminal window # 2. stdout to a pipe # wrap everything to catch Ctrl-C try: # if we have files, parse them into old and new ClassAds if FILES: if len(FILES) != 2: sys.stderr.write(('Error: Bad arguments or more/less ' 'than 2 files passed:\n')) sys.stderr.write(' '.join(FILES)) sys.stderr.write('\n') sys.exit(1) try: with open(FILES[0]) as f: old = classad.parseNext(f) if not old: sys.stderr.write(( 'Error: {0} does not contain any (valid) ClassAds\n' ).format(FILES[0])) except Exception as e: sys.stderr.write(str(e)) sys.stderr.write('\n') sys.exit(1) try: with open(FILES[1]) as f: new = classad.parseNext(f) if not new: sys.stderr.write(( 'Error: {0} does not contain any (valid) ClassAds\n' ).format(FILES[1])) except Exception as e: sys.stderr.write(str(e)) sys.stderr.write('\n') sys.exit(1) # make sure new is newer than old if new['MyCurrentTime'] < old['MyCurrentTime']: old, new = (new, old) # get the daemon type types = { 'Collector': 'Collector', 'DaemonMaster': 'Master', 'Negotiator': 'Negotiator', 'Scheduler': 'Schedd', 'Machine': 'Startd' } if new['MyType'] in types: DAEMON = types[new['MyType']] else: DAEMON = new['MyType'] # get the daemon name NAME = new['Name'] # if we're querying the Collector, first get the "old" ClassAd else: if NAME is None: NAME = getDaemonName(POOL, DAEMON, MACHINE) old = queryClassAd(POOL, DAEMON, NAME, DIRECT) # go ahead and print the information we have printMonitorSelf(DAEMON, NAME, old) printRecentHealthStats(old) # if there aren't any runtime attributes in the classad, # print the information we have and exit now if len(getRuntimeAttrs(old)) == 0: sys.stderr.write("Did not get any comparable runtime attributes from daemon ClassAd, exiting early.\n") sys.exit(0) # countdown and get the "new" ClassAd from the Collector printCountdown() new = queryClassAd(POOL, DAEMON, NAME, DIRECT) # if in live mode, clear the terminal if LIVE: resetTerminal() pass # at this point, we have both old and new ClassAds, so print the info lines = printInstHealthStats(old, new) printRuntimeTable(old, new, lines = lines) # if in live mode, continue getting new ClassAds if LIVE: while True: old = deepcopy(new) printCountdown() new = queryClassAd(POOL, DAEMON, NAME, DIRECT) resetTerminal() lines = printMonitorSelf(DAEMON, NAME, new) lines += printInstHealthStats(old, new) printRuntimeTable(old, new, lines = lines) except KeyboardInterrupt: sys.stdout.write('\n') sys.stdout.flush()