#!/usr/bin/env python # coding: utf-8 # Copyright (C) 1994-2018 Altair Engineering, Inc. # For more information, contact Altair at www.altair.com. # # This file is part of the PBS Professional ("PBS Pro") software. # # Open Source License Information: # # PBS Pro is free software. You can redistribute it and/or modify it under the # terms of the GNU Affero General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # PBS Pro is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. # See the GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # # Commercial License Information: # # For a copy of the commercial license terms and conditions, # go to: (http://www.pbspro.com/UserArea/agreement.html) # or contact the Altair Legal Department. # # Altair’s dual-license business model allows companies, individuals, and # organizations to create proprietary derivative works of PBS Pro and # distribute them - whether embedded or bundled with other software - # under a commercial license agreement. # # Use of Altair’s trademarks, including but not limited to "PBS™", # "PBS Professional®", and "PBS Pro™" and Altair’s logos is subject to Altair's # trademark licensing policies. import getopt import sys import os import traceback import logging import logging.config import time import errno import ptl from ptl.utils.pbs_logutils import PBSLogUtils, PBSLogAnalyzer from ptl.utils.pbs_cliutils import CliUtils from ptl.utils.plugins.ptl_test_db import PTLTestDb from ptl.lib.pbs_testlib import PtlConfig # trap SIGINT and SIGPIPE def trap_exceptions(etype, value, tb): sys.excepthook = sys.__excepthook__ if issubclass(etype, KeyboardInterrupt): pass elif issubclass(etype, IOError) and value.errno == errno.EPIPE: pass else: sys.__excepthook__(etype, value, tb) sys.excepthook = trap_exceptions def usage(): msg = [] msg += ['Usage: ' + os.path.basename(sys.argv[0]).split('.pyc')[0]] msg += [' [OPTION]\n\n'] msg += [' Analyze PBS logs and return various throughput metrics\n\n'] msg += ['-a : path to accounting log file/dir to analyze\n'] msg += ['-b: process log from corresponding begin/start time\n'] msg += [' format: %m/%d/%Y %H:%M:%S\n'] msg += ['-c: output cycle summary\n'] msg += ['-d : path to a pbs_diag directory\n'] msg += ['-e: process log up to corresponding end time\n'] msg += [' format: %m/%d/%Y %H:%M:%S\n'] msg += ['-f : generic log file for analysis\n'] msg += ['-h: display usage information\n'] msg += ['-t : hostname. Defaults to FQDN local hostname\n'] msg += ['-l : path to scheduler log file/dir to analyze\n'] msg += ['-m : path to mom log file/dir to analyze\n'] msg += ['-s : path to server log file/dir to analyze\n'] msg += ['-S: show per job scheduling details, time to ' 'run/discard/calendar\n'] msg += ['-U: show utilization. Requires paths to jobs and nodes info\n'] msg += ['--estimated-info: show job start time estimate info. ' 'Requires scheduler log(s)\n'] msg += ['--estimated-info-only: write only estimated info to the DB.' ' Requires --db-out\n'] msg += ['--last-week: analyze logs of the last 7 days\n'] msg += ['--last-month: analyze logs of the last month\n'] msg += ['--re-interval=: report time interval between ' 'occurrences of regexp\n'] msg += ['--re-frequency=: report frequency of occurrences of ' 'the re-interval\n'] msg += [' expression for every \n'] msg += ['--silent: do not display progress bar. Defaults to False\n'] msg += ['--log-conf=: logging config file\n'] msg += ['--nodes-file=: path to file with output of pbsnodes -av\n'] msg += ['--jobs-file=: path to file with output of qstat -f\n'] msg += ['--db-out=: send results to db file\n'] msg += ['--db-type=: database type\n'] msg += ['--db-access=: Path to a file that defines db options ' '(PostreSQL only)\n'] msg += ['--version: print version number and exit\n'] print "".join(msg) if __name__ == '__main__': if len(sys.argv) < 2: usage() sys.exit(0) diag = None schedulerlog = None serverlog = None momlog = None acctlog = None genericlog = None hostname = None sj = False compact = False begin = None end = None cyclesummary = False nodesfile = None jobsfile = None utilization = None silent = False logconf = None estimated_info = False estimated_info_only = False dbout = None dbtype = None dbaccess = None re_interval = None re_frequency = None re_conditional = None json_on = False level = logging.FATAL logutils = PBSLogUtils() dbutils = PTLTestDb() try: shortopt = "a:b:d:e:f:t:l:L:s:m:cShU" longopt = ["nodes-file=", "jobs-file=", "version", "log-conf=", "estimated-info", "db-out=", "json", "re-interval=", "re-frequency=", "last-week", "last-month", "re-conditional=", "estimated-info-only", "silent", "db-type=", "db-access="] opts, args = getopt.getopt(sys.argv[1:], shortopt, longopt) except: usage() sys.exit(1) for o, val in opts: if o == '-a': acctlog = CliUtils.expand_abs_path(val) elif o == '-b': try: begin = logutils.convert_date_time(val) except: print('Error converting time, expected format ' '%m/%d/%Y %H:%M:%S') sys.exit(1) elif o == '-e': try: end = logutils.convert_date_time(val) except: print('Error converting time, expected format ' '%m/%d/%Y %H:%M:%S') print traceback.print_exc() sys.exit(1) elif o == '-d': diag = CliUtils.expand_abs_path(val) elif o == '-f': genericlog = CliUtils.expand_abs_path(val) elif o == '-t': hostname = val elif o == '-l': schedulerlog = CliUtils.expand_abs_path(val) elif o == '-s': serverlog = CliUtils.expand_abs_path(val) elif o == '-m': momlog = CliUtils.expand_abs_path(val) elif o == '-c': cyclesummary = True elif o == '-C': compact = True elif o == '-L': level = CliUtils.get_logging_level(val) elif o == '-S': sj = True elif o == '-U': utilization = True elif o == '--db-out': dbout = CliUtils.expand_abs_path(val) elif o == '--db-type': dbtype = val elif o == '--db-access': dbaccess = CliUtils.expand_abs_path(val) elif o == '--estimated-info': estimated_info = True elif o == '--estimated-info-only': estimated_info_only = True elif o == '--json': json_on = True elif o == '--last-week': s = time.localtime(time.time() - (7 * 24 * 3600)) begin = int(time.mktime(time.strptime(time.strftime("%m/%d/%Y", s), "%m/%d/%Y"))) end = int(time.time()) elif o == '--last-month': s = time.localtime(time.time() - (30 * 24 * 3600)) begin = int(time.mktime(time.strptime(time.strftime("%m/%d/%Y", s), "%m/%d/%Y"))) end = int(time.time()) elif o == '--log-conf': logconf = CliUtils.expand_abs_path(val) elif o == '--nodes-file': nodesfile = CliUtils.expand_abs_path(val) elif o == '--jobs-file': jobsfile = CliUtils.expand_abs_path(val) elif o == '--re-conditional': re_conditional = eval(val, {}, {}) elif o == '--re-interval': re_interval = val elif o == '--silent': silent = True elif o == '--re-frequency': re_frequency = int(val) elif o == '--version': print ptl.__version__ sys.exit(0) elif o == '-h': usage() sys.exit(0) else: sys.stderr.write("Unrecognized option " + o) usage() sys.exit(1) if logconf: logging.config.fileConfig(logconf) else: logging.basicConfig(level=level) PtlConfig() if diag: if nodesfile is None: if os.path.isfile(os.path.join(diag, 'pbsnodes_va.out')): nodesfile = os.path.join(diag, 'pbsnodes_va.out') if jobsfile is None: if os.path.isfile(os.path.join(diag, 'qstat_f.out')): jobsfile = os.path.join(diag, 'qstat_f.out') if ((re_interval is not None or re_conditional is not None) and genericlog is None): if schedulerlog is not None: genericlog = schedulerlog schedulerlog = None elif serverlog is not None: genericlog = serverlog serverlog = None elif momlog is not None: genericlog = momlog momlog = None elif acctlog is not None: genericlog = acctlog acctlog = None show_progress = not silent pla = PBSLogAnalyzer(schedulerlog, serverlog, momlog, acctlog, genericlog, hostname, show_progress) if utilization: if acctlog is None: logging.error("Accounting log is required to compute utilization") sys.exit(1) pla.accounting.enable_utilization_parsing(hostname, nodesfile, jobsfile) if re_interval is not None: pla.set_custom_match(re_interval, re_frequency) if re_conditional is not None: pla.set_conditional_match(re_conditional) if estimated_info or estimated_info_only: if schedulerlog is None: logging.error("Scheduler log is required for estimated start time " "analysis") sys.exit(1) pla.scheduler.estimated_parsing_enabled = True if estimated_info_only: pla.scheduler.parse_estimated_only = True info = pla.analyze_logs(start=begin, end=end, showjob=sj) if genericlog: dbutils.process_output(pla.info) # Drift analysis and custom regex matching require additional # post-processing and can't currently be passed through to JSON if json_on: if cyclesummary: info['scheduler'] = info['scheduler']['summary'] print CliUtils.__json__(info) sys.exit(0) if acctlog: dbutils.process_output(info['accounting'], dbout, dbtype, dbaccess, name=acctlog, logtype='accounting') if schedulerlog: dbutils.process_output(info['scheduler'], dbout, dbtype, dbaccess, name=schedulerlog, logtype='scheduler', summary=cyclesummary) if serverlog: dbutils.process_output(info['server'], dbout, dbtype, dbaccess, name=serverlog, logtype='server') if momlog: dbutils.process_output(info['mom'], dbout, dbtype, dbaccess, name=momlog, logtype='mom')