#!/usr/bin/env python # NBTop v0.1a interactive job monitor # Copyright (c) 2002, George Schlossnagle. All rights reserved # This program is free software and can be redistibuted and modified # as you wish, as long as this header remains in place. # This program is provided "as is" and without any express or implied # warranties. If it breaks, you can have bothe pieces. import curses import glob import os import popen2 import UserDict import select import time import types import re import getopt import sys import string import stat class TailFile: def __init__(self, filename, follow = 0): self.filename = filename if not os.path.exists(filename): print "no such file: '"+filename+"'." sys.exit(1) if os.path.isdir(filename): print "file '"+filename+"' is a directory." sys.exit(1) if not os.access(filename, os.R_OK): print "cannot read file '"+filename+"'. permission denied." sys.exit(1) self.file = open(self.filename, 'r') self.follow = follow self.readbuf = "" def readline(self): index = string.find(self.readbuf, '\n') while index == -1: try: n = self.more_to_read() except Exception: return None if n == 0: if self.follow: time.sleep(1) else: return None else: self.readbuf = self.readbuf + self.file.read(n) index = string.find(self.readbuf, '\n') line = self.readbuf[:index+1] self.readbuf = self.readbuf[index+1:] return line def seek_lines(self, lines): bufsize = 2048 bytestoread = 2048 self.file.seek(0, 2) if lines == 0: return pos = self.file.tell() while pos != 0: pos = pos - bufsize if pos < 0: bytestoread = bytestoread + pos pos = 0 self.file.seek(pos) buf = self.file.read(bytestoread) j = bytestoread-1 while j >= 0: if buf[j] == '\n': lines = lines - 1 if lines < 0: self.file.seek(pos+j+1) return j = j - 1 self.file.seek(0) def end_of_file_position(self): return os.stat(self.filename)[stat.ST_SIZE] def more_to_read(self): end = self.end_of_file_position() if end < self.file.tell(): self.file.seek(end) return 0 return end - self.file.tell() class TextWindow: def __init__(self,a,b,c,d): self.window = curses.newwin(a,b,c,d) self.height = a self.width = b self.top = c self.lhs = d self.window.leaveok(1) self.display = self._defaultDisplay global dict def topMenu(self): if dict.currentLine != -1: if dict.jobs[dict.currentLine].status != 'Active': dict.jobs[dict.currentLine].readData(1000) else: dict.jobs[dict.currentLine].readData(0) obj = dict.jobs[dict.currentLine] self.window.erase() self.window.addstr(0,0, "JOB ID:", curses.A_BOLD) self.window.addstr(0,8, str(obj.pid)) self.window.addstr(0,16, "STATUS:", curses.A_BOLD) # self.window.addstr(0,24, obj.status) if obj.retval == 0: status = "SUCCESS" elif obj.retval == None: status = "RUNNING" else: status = "FAILED" self.window.addstr(0, 24, status) self.window.addstr(2,0, "CLASS: ", curses.A_BOLD) self.window.addstr(2,8, obj.nbclass) self.window.addstr(3,0, "SCHEDULE:", curses.A_BOLD) self.window.addstr(3,10, obj.schedule) if obj.startTime: self.window.addstr(1, 0, "STARTED: ", curses.A_BOLD) self.window.addstr(1, 10, time.strftime("%Y-%m-%d %H:%M:%S" , time.localtime(obj.startTime))) if obj.totalBytes: self.window.addstr(4,0, "Mbytes Written (total): ", curses.A_BOLD) self.window.addstr(4,24,str(obj.totalBytes/1024)) self.window.addstr(4,32, "Avg. Xfer Rate: ", curses.A_BOLD) if obj.now - obj.startTime != 0: self.window.addstr(4,48, "%6.2f" % (float(obj.totalBytes)/(1000.0*float(obj.now - obj.startTime))) + " MB/s") if obj.now and obj.startTime: self.window.addstr(5,0, "Elapsed Time: ", curses.A_BOLD) self.window.addstr(5,16, str((obj.now - obj.startTime)/60)+ " min") self.window.addstr(5,32, "Files Written: ", curses.A_BOLD) self.window.addstr(5,47, str(obj.filesWritten)) else: self.window.erase() self.window.addstr(1,self.width/2 - 7, "NBTop - v0.1a", curses.A_BOLD) self.window.addstr(self.height - 1,0, '-' * (self.width - 1)) self.window.refresh() def bottomMenu(self): self.window.addstr(0,0, "Press ? for help, q to quit.") self.window.refresh() def _defaultDisplay(self): pass class MainWin: def __init__(self, a,b,c,d): self.window = curses.newwin(a,b,c,d) self.height = a self.width = b self.top = c self.lhs = d self.window.leaveok(1) global dict dict.jobs.sort(NBJob.sortbystatus) self.display = self.overview self.processNormalKeys = self._processNormalKeys self.processSpecialKeys = self._processSpecialKeys def overview(self): self.window.addstr(0,0, \ " jobid type status class schedule host", curses.A_BOLD) self.window.addstr(1,0, '-' * (self.width - 1)) i = 0 for obj in dict.jobs[0:self.height - 3]: self.window.move(i + 2,0) self.window.clrtoeol() if i == dict.currentLine: self.window.addstr(i + 2,0,str(obj), curses.A_REVERSE) else: self.window.addstr(i + 2,0,str(obj)) i = i + 1 self.window.refresh() def jobZoom(self): self.window.erase() dict.jobs[dict.currentLine].readData(1000) obj = dict.jobs[dict.currentLine] if obj.fd: self.window.addstr(0, 0, "Job Detail", curses.A_BOLD) now = int(time.time()) # Connect Time if obj.connectTime is not None: self.window.addstr(1, 0, "Connect Time: "+ str(obj.connectTime) + " seconds") elif obj.connectStart is not None: self.window.addstr(1, 0, "Connect Time: "+ str(now - obj.connectStart) + " seconds") else: self.window.addstr(1, 0, "Connect Time: 0 seconds") # Mount time if obj.mountTime is not None: self.window.addstr(1, 30, "Mount Time: "+ str(obj.mountTime) + " seconds") elif obj.mountStart is not None: self.window.addstr(1, 30, "Mount Time: "+ str(now - obj.mountStart) + " seconds") else: self.window.addstr(1, 0, "Mount Time: 0 seconds") # Position Time if obj.positionTime is not None: self.window.addstr(2, 0, "Position Time: "+ str(obj.positionTime) + " seconds") elif obj.positionStart is not None: self.window.addstr(2, 0, "Position Time: "+ str(now - obj.positionStart) + " seconds") else: self.window.addstr(2, 0, "Position Time: 0 seconds") # Write Time if obj.writeTime is not None: self.window.addstr(2, 30, "Write Time: "+ str(obj.writeTime) + " seconds") elif obj.writeStart is not None: self.window.addstr(2, 30, "Write Time: "+ str(now - obj.writeStart) + " seconds") else: self.window.addstr(2, 30, "Write Time: 0 seconds") i = 3 if obj.status == 'Active': self.window.addstr(i,0, "This File Backup Status", curses.A_BOLD) i = i + 1 if obj.lastFile: self.window.addstr(i, 0, "Last File Backed Up: " + obj.lastFile) i = i + 1 if obj.thisFileStart and obj.thisFileBytes: rate = "%6.2f" % (float(obj.thisFileBytes)/float(1000*(now - obj.thisFileStart))) self.window.addstr(i,30, "This File Xfer Rate: "+ str(rate) + "Mb/s") self.window.addstr(i,0, "File MB: " + str(obj.thisFileBytes/1000)) i = i + 1 self.window.addstr(i, 0, "Media Used", curses.A_BOLD) i = i + 1 for tape in obj.mountMedia: self.window.addstr(i, 4, tape) i = i + 1 self.window.addstr(i, 0, "Paths Backed Up", curses.A_BOLD) i = i + 1 if obj.filelist: for file in obj.filelist: if i == self.height - 1: break self.window.addstr(i, 0, file) i = i + 1 else: self.window.addstr(1, 0, "Job Details Unavailable (log files expired)") self.window.refresh() def about(self): self.window.erase() self.window.addstr(0,0, "NBTop was written by George Schlossnagle .") self.window.addstr(1,0, "This software is free and provided with no warranty.") self.window.addstr(3,0, "Press 'o' to return to the job overview screen") self.window.refresh() def help(self): self.window.erase() self.window.addstr(1,0, "a - About the software") self.window.addstr(2,0, "o - Job Overview showing all jobs") self.window.addstr(3,0, "? - Job Overview showing all jobs") self.window.addstr(4,0, "Arrow Keys - Scroll through jobs, zoom in and zoom out") self.window.refresh() def _processNormalKeys(self, c): global dict if c == 'a': self.display = self.about elif c == 'o': self.display = self.overview elif c == 'r': dict = NBDict() elif c == '?': self.display = self.help else: pass def _processSpecialKeys(self, c): if c == curses.KEY_DOWN and dict.currentLine < self.height - 4: dict.currentLine = dict.currentLine + 1 elif c == curses.KEY_UP and dict.currentLine >= 0: dict.currentLine = dict.currentLine - 1 elif c == curses.KEY_RIGHT and dict.currentLine >= 0: self.display = self.jobZoom elif c == curses.KEY_LEFT: self.display = self.overview else: pass self.window.refresh() class StatsWindow: def __init__(self): self.scr = curses.initscr() self.scr.nodelay(1) self.scr.keypad(1) self.scr.leaveok(1) self.menuwin = TextWindow(7,curses.COLS - 1,0,0) self.menuwin.display = self.menuwin.topMenu self.mainwin = MainWin(curses.LINES - 7,curses.COLS - 1,7,0) self.bottomwin = TextWindow(1,curses.COLS-1,curses.LINES - 1,0) self.bottomwin.display = self.bottomwin.bottomMenu def display(self): self.menuwin.display() self.mainwin.display() self.bottomwin.display() def start(self): while 1: self.display() if self.processKeys() < 0: break time.sleep(0.01) def processKeys(self): c = self.scr.getch() if 0 incremental: break j = j + 1 def parseLine(self, line): match = re.search("^BEGIN_WRITING (\d+)", line) if match: self.thisFileStart = int(match.group(1)) self.writeStart = int(match.group(1)) return match = re.search("^KBW (\d+) (\d+)", line) if match: self.thisFileBytes = self.thisFileBytes + int(match.group(2)) self.totalBytes = self.totalBytes + int(match.group(2)) self.now = int(match.group(1)) return match = re.search("^PATH_WRITTEN (\d+) (\S+)", line) if match: self.now = int(match.group(1)) self.lastFile = match.group(2) self.thisFileStart = int(match.group(1)) self.thisFileBytes = 0 return match = re.search("^FW (\d+) (\d+)", line) if match: self.now = int(match.group(1)) self.filesWritten += int(match.group(2)) return match = re.search("^END_WRITING (\d+)", line) if match: self.now = int(match.group(1)) self.writeFinish = self.now if self.writeStart: self.writeTime = self.writeFinish - self.writeStart return match = re.search("^PROCESS (\d+)", line) if match: self.startTime = int(match.group(1)) return match = re.search("^CONNECT (\d+)", line) if match: self.connectStart = int(match.group(1)) return match = re.search("^CONNECTED (\d+)", line) if match: self.connectFinish = int(match.group(1)) self.connectTime = self.connectFinish - self.connectStart return match = re.search("^MOUNTING (\d+) (\S+)", line) if match: self.mountStart = int(match.group(1)) self.mountMedia.append(match.group(2)) return match = re.search("^MOUNTED (\d+)", line) if match: self.mountFinish = int(match.group(1)) self.mountTime = self.mountFinish - self.mountStart return match = re.search("^POSITIONING (\d+)", line) if match: self.positionStart = int(match.group(1)) return match = re.search("^POSITIONED (\d+)", line) if match: self.positionFinish = int(match.group(1)) self.positionTime = self.positionFinish - self.positionStart return def sortbystatus(a,b): return cmp(a.status, b.status) or cmp(b.pid, a.pid) class NBDict: def __init__(self): self.jobs = self._get_jobs() self.sortfunc = NBJob.sortbystatus self.jobs.sort(self.sortfunc) self.currentLine = -1 def _get_jobs(self): fd = os.popen(bpdbjobs+" -report", "r") array = [] while 1: line = fd.readline() if not line: break array.append(NBJob(line)) return array def usage(): print """ NBTop (v0.1a) - A curses based interactive job browser for Veritas NetBackup If Netbackup is installed in /usr/openv/ you should be set. Otherwise you should set --bpdbjobs=/usr/openv/nebackup/bin/admincmd/bpdbjobs --jobsdb=/usr/openv/nebackup/db/jobs/ to values appropriate for your install. Online help is available by pressing '?' """ sys.exit(2) ############################################# MAIN ######################################### # Configuration Defines bpdbjobs = '/usr/openv/netbackup/bin/admincmd/bpdbjobs' jobs_path = '/usr/openv/netbackup/db/jobs/' try: opts, args = getopt.getopt(sys.argv[1:], "h", ['--bpdbjobs=', '--jobsdb=', '--help']) except getopt.GetoptError: usage() for key, value in opts: if key in ("--help", "-h"): usage() if key == '--bpdbjobs': bpdbjobs = value if key == '--jobsdb': jobs_path = value if os.getuid() != 0: print "You must be root to run NBTo. Your uid is " + str(os.getuid()) sys.exit(1) dict = NBDict() window = StatsWindow() try: curses.def_shell_mode() curses.noecho() ; curses.cbreak() window.start() finally: curses.reset_shell_mode() curses.echo() if not curses.isendwin(): curses.endwin()