308 lines
11 KiB
Python
Executable file
308 lines
11 KiB
Python
Executable file
#!/usr/bin/env python
|
|
|
|
# Copyright (c) 2009 Timo Makinen
|
|
# All rights reserved.
|
|
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions
|
|
# are met:
|
|
# 1. Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# 2. Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution.
|
|
# 3. The name of the author may not be used to endorse or promote products
|
|
# derived from this software without specific prior written permission.
|
|
|
|
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
|
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
|
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
|
|
import cups
|
|
from optparse import OptionParser
|
|
import os
|
|
import re
|
|
from subprocess import Popen, PIPE
|
|
import sys
|
|
from time import strftime
|
|
|
|
|
|
def main():
|
|
os.environ['LANG'] = 'en_US'
|
|
parser = OptionParser()
|
|
parser.add_option('-v', '--verbose',
|
|
action='store_true', dest='verbose', default=False,
|
|
help='run in verbose mode')
|
|
parser.add_option('-t', '--templates',
|
|
dest='templates', default='/usr/share/cups/templates',
|
|
help='cups template directory')
|
|
parser.add_option('-d', '--datadir',
|
|
dest='datadir', default='/usr/share/doc/cups-1.3.7/details',
|
|
help='directory where to store html pages')
|
|
(options, args) = parser.parse_args()
|
|
|
|
c = cups.Connection()
|
|
printers = []
|
|
for (name, details) in c.getPrinters().items():
|
|
if len(args) > 0 and name not in args:
|
|
continue
|
|
m = re.match('^([a-z]+)://(\S+):\d+.*', details['device-uri'])
|
|
if m is None or m.group(1) not in ('http', 'ipp', 'lpd', 'socket'):
|
|
continue
|
|
printers.append( (name, m.group(2)) )
|
|
|
|
for (name, addr) in printers:
|
|
if options.verbose:
|
|
print 'retrieving data for printer %s (%s)' % (name, addr)
|
|
data = getdata(name, addr, options.templates)
|
|
outfile = os.path.join(options.datadir, '%s.html' % name)
|
|
if data is None:
|
|
if options.verbose:
|
|
print ' failed to retrieve snmp data'
|
|
print ' updating last check timestamp'
|
|
data = ''
|
|
r = re.compile('(<strong>Last updated: </strong>\d+-\d+-\d+ \d+:\d+:\d+)(.*)<br />')
|
|
try:
|
|
for l in open(outfile, 'r').readlines():
|
|
l = r.sub(r'\1 (last checked ' \
|
|
+ strftime('%Y-%m-%d %H:%M:%S') \
|
|
+ ')<br />\n', l)
|
|
data += l
|
|
except IOError:
|
|
continue
|
|
else:
|
|
if options.verbose:
|
|
print ' saving data to %s' % outfile
|
|
savedata(outfile, data)
|
|
|
|
|
|
def getdata(name, addr, templates):
|
|
data = readtemplate(os.path.join(templates, 'header.tmpl'),
|
|
title='%s details' % name)
|
|
data += '<h1>%s</h1>\n' % name
|
|
|
|
oids = (
|
|
('Product Name', '.1.3.6.1.2.1.25.3.2.1.3.1'),
|
|
('Printer Serial Number', '.1.3.6.1.2.1.43.5.1.1.17.1'),
|
|
('Page Count', '.1.3.6.1.2.1.43.10.2.1.4.1.1'),
|
|
)
|
|
snmpdata = snmpget('public', addr, *map(lambda x: x[1], oids))
|
|
if len(snmpdata) == 0:
|
|
return None
|
|
data += '<h2>Printer Information</h2>\n'
|
|
data += '<p>\n'
|
|
for (k, v) in oids:
|
|
try:
|
|
data += '<strong>%s: </strong>%s<br />\n' % (k, snmpdata[v])
|
|
except KeyError:
|
|
pass
|
|
snmpdata = snmpwalk('public', addr, '.1.3.6.1.2.1.43.13.4.1.10')
|
|
duplex = 'Not installed'
|
|
for key, value in snmpdata.items():
|
|
if re.search('Duplex', value):
|
|
duplex = 'Installed'
|
|
data += '<strong>Duplexer: </strong>%s<br />\n' % duplex
|
|
snmpdata = snmpwalk('public', addr, '.1.3.6.1.4.1.11.2.3.9.4.2.1.1.4.1')
|
|
totalmemory = 0
|
|
memoryslots = []
|
|
for key, value in snmpdata.items():
|
|
if re.match('^\.1\.3\.6\.1\.4\.1\.11\.2\.3\.9\.4\.2\.1\.1\.4\.1\.\d+\.5\.0$', key):
|
|
totalmemory += int(value) / 1024 / 1024
|
|
memoryslots.append('%s MB' % str(int(value) / 1024 / 1024))
|
|
if totalmemory > 0:
|
|
data += '<strong>Installed memory: </strong>%s MB total (%s)<br />\n' % (
|
|
totalmemory, ', '.join(memoryslots))
|
|
data += '<strong>Last updated: </strong>%s<br />\n' % (
|
|
strftime('%Y-%m-%d %H:%M:%S'))
|
|
data += '</p>\n'
|
|
data += '\n'
|
|
|
|
baseoid = '.1.3.6.1.2.1.43.11.1.1'
|
|
data += '<h2>Supplies</h2>\n'
|
|
snmpdata = snmpwalk('public', addr, baseoid)
|
|
keys = map(lambda x: x.split('.')[-1], filter(lambda x: x.startswith(
|
|
baseoid + '.6.1'), snmpdata.keys()))
|
|
data += '<table>\n'
|
|
for k in keys:
|
|
try:
|
|
value = ' %s\n' % getstatus(snmpdata['%s.9.1.%s' % (baseoid, k)],
|
|
snmpdata['%s.8.1.%s' % (baseoid, k)])
|
|
except KeyError:
|
|
continue
|
|
data += '<tr>\n'
|
|
data += ' <td>%s:</td>\n' % (
|
|
snmpdata[baseoid + '.6.1.' + k])
|
|
data += value
|
|
data += '</tr>\n'
|
|
data += '</table>\n'
|
|
data += '\n'
|
|
|
|
baseoid = '.1.3.6.1.2.1.43.8.2.1'
|
|
data += '<h2>Paper trays</h2>\n'
|
|
snmpdata = snmpwalk('public', addr, baseoid)
|
|
keys = list(set(map(lambda x: int(x.split('.')[-1]), snmpdata.keys())))
|
|
data += '<table>\n'
|
|
for idx in keys:
|
|
data += '<tr>\n'
|
|
if '%s.18.1.%s' % (baseoid, idx) in snmpdata:
|
|
description = snmpdata['%s.18.1.%s' % (baseoid, idx)]
|
|
elif '%s.13.1.%s' % (baseoid, idx) in snmpdata:
|
|
description = snmpdata['%s.13.1.%s' % (baseoid, idx)]
|
|
else:
|
|
description = 'Unknown'
|
|
if '%s.12.1.%s' % (baseoid, idx) in snmpdata:
|
|
description += ' (%s)' % snmpdata['%s.12.1.%s' % (baseoid, idx)]
|
|
data += ' <td>%s</td>\n' % description
|
|
data += ' %s' % getstatus(snmpdata['%s.10.1.%s' % (baseoid, idx)],
|
|
snmpdata['%s.9.1.%s' % (baseoid, idx)])
|
|
data += '</tr>\n'
|
|
data += '</table>\n'
|
|
data += '\n'
|
|
|
|
baseoid = '.1.3.6.1.4.1.11.2.3.9.4.2.1.1.11'
|
|
snmpdata = snmpwalk('public', addr, baseoid)
|
|
keys = list(set(map(lambda x: int(x.split('.')[-3]), snmpdata.keys())))
|
|
if len(keys) > 0:
|
|
data += '<h2>Event Log</h2>\n'
|
|
keys.sort(lambda x, y: y-x)
|
|
data += '<table>\n'
|
|
data += '<tr>\n'
|
|
data += ' <th>Number</th>\n'
|
|
data += ' <th>Page Count</th>\n'
|
|
data += ' <th>Error code</th>\n'
|
|
data += '</tr>\n'
|
|
for idx in keys:
|
|
if snmpdata['%s.%s.1.0' % (baseoid, idx)] == 0:
|
|
continue
|
|
data += '<tr>\n'
|
|
data += ' <td>%s</td>\n' % idx
|
|
data += ' <td>%s</td>\n' % snmpdata['%s.%s.1.0' % (baseoid, idx)]
|
|
error = hex(snmpdata['%s.%s.2.0' % (baseoid, idx)])
|
|
if len(error) == 7:
|
|
error = '%s.%s.%s\n' % (str(long(error[0:3], 16)),
|
|
error[3:5], error[5:7])
|
|
else:
|
|
error = '%s.%s.%s\n' % (str(long(error[0:4], 16)),
|
|
error[4:6], error[6:8])
|
|
data += ' <td>%s</td>\n' % error.upper()
|
|
data += '</tr>\n'
|
|
data += '</table>\n'
|
|
data += '\n'
|
|
|
|
data += readtemplate(os.path.join(templates, 'trailer.tmpl'))
|
|
return data
|
|
|
|
|
|
def getstatus(cur, max):
|
|
if cur > 0 and max > 0:
|
|
used = cur*100/max
|
|
ret = ' <td>%s percent left</td>\n' % used
|
|
ret += ' <td style="width: 100px; border: 1px solid black;">\n'
|
|
if used < 10:
|
|
color = 'red'
|
|
elif used < 30:
|
|
color = 'yellow'
|
|
else:
|
|
color = 'green'
|
|
ret += ' <div style="background: %s; width: %spx;"> </div>\n' % (
|
|
color, used)
|
|
ret += ' </td>\n'
|
|
else:
|
|
if cur == -3:
|
|
cur = '<div style="color: green;">OK</div>'
|
|
elif cur == -2:
|
|
cur = 'OK'
|
|
elif cur == 0:
|
|
cur = '<div style="color: red;">Empty</div>'
|
|
ret = ' <td colspan="2">%s</td>\n' % cur
|
|
return ret
|
|
|
|
|
|
def savedata(name, data):
|
|
f = open(name, 'w')
|
|
f.write(data)
|
|
f.close()
|
|
os.chmod(name, 0644)
|
|
|
|
|
|
## cups related functions
|
|
def readtemplate(name, *args, **kwargs):
|
|
f = open(name, 'r')
|
|
ret = ''
|
|
for line in f.readlines():
|
|
for (k, v) in kwargs.items():
|
|
line = re.sub('{%s}' % k, v, line)
|
|
line = re.sub('{.+\?.+:}', '', line)
|
|
ret += line
|
|
f.close()
|
|
return ret
|
|
|
|
|
|
## snmp functions
|
|
def snmpget(community, hostname, *args):
|
|
cmdline = [ 'snmpget', '-Ona', '-v', '1', '-c', community, hostname ]
|
|
for o in args:
|
|
cmdline.append(o)
|
|
ret = {}
|
|
cmd = Popen(cmdline, stdout=PIPE, stderr=PIPE, close_fds=True)
|
|
for line in cmd.stdout.readlines():
|
|
line = snmpparse(line)
|
|
if line is not None:
|
|
ret[line[0]] = line[1]
|
|
return ret
|
|
|
|
|
|
def snmpwalk(community, hostname, oid):
|
|
cmdline = [ 'snmpwalk', '-Ona', '-v', '1', '-c', community, hostname, oid ]
|
|
ret = {}
|
|
cmd = Popen(cmdline, stdout=PIPE, stderr=PIPE, close_fds=True)
|
|
buff = ''
|
|
for line in cmd.stdout.readlines():
|
|
if re.search('[\.0-9]* = .*', line):
|
|
buff = snmpparse(buff)
|
|
if buff is not None:
|
|
ret[buff[0]] = buff[1]
|
|
buff = line.strip()
|
|
else:
|
|
buff += line.strip()
|
|
buff = snmpparse(buff)
|
|
if buff is not None:
|
|
ret[buff[0]] = buff[1]
|
|
return ret
|
|
|
|
|
|
def snmpparse(data):
|
|
data = data.strip().split(' ', 3)
|
|
try:
|
|
if data[2] in ('INTEGER:', 'Counter32:'):
|
|
ret = int(data[3])
|
|
elif data[2] == 'STRING:':
|
|
if data[3][0] == '"' and data[3][-1] == '"':
|
|
data[3] = data[3][1:-1]
|
|
ret = data[3]
|
|
elif data[2] == 'Hex-STRING:':
|
|
ret = data[3]
|
|
elif data[2] == 'NULL':
|
|
ret = None
|
|
elif data[2] == '""':
|
|
return None
|
|
else:
|
|
raise ValueError('Invalid value type %s' % data[2][0:-1])
|
|
except IndexError:
|
|
return None
|
|
return (data[0], ret)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt:
|
|
sys.exit(1)
|