mirror of
https://github.com/munin-monitoring/contrib.git
synced 2018-11-08 00:59:34 +01:00
451af405b7
preparation for an additional file permission test (based on extensions)
966 lines
34 KiB
Python
Executable File
966 lines
34 KiB
Python
Executable File
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
A munin plugin that prints archive and their upgradable packets
|
|
|
|
TODO: make it usable and readable as commandline tool
|
|
• (-i) interaktiv
|
|
NICETOHAVE
|
|
TODO: separate into 2 graphs
|
|
• how old is my deb installation
|
|
sorting a packet to the oldest archive
|
|
sorting a packet to the newest archive
|
|
(WONTFIX unless someone asks for)
|
|
|
|
TODO:
|
|
• addinge alternative names for archives "stable -> squeeze"
|
|
TODO: add gray as
|
|
foo.colour 000000
|
|
to 'now', '', '', '', '', 'Debian dpkg status file'
|
|
TODO: update only if system was updated (aptitutde update has been run)
|
|
• check modification date of /var/cache/apt/pkgcache.bin
|
|
• cache file must not be older than mod_date of pkgcache.bin + X
|
|
TODO: shorten ext_info with getShortestConfigOfOptions
|
|
TODO: check whether cachefile matches the config
|
|
• i have no clever idea to do this without 100 lines of code
|
|
BUG: If a package will be upgraded, and brings in new dependencies,
|
|
these new deps will not be counted. WONTFIX
|
|
"""
|
|
import sys
|
|
import argparse
|
|
import apt_pkg
|
|
from apt.progress.base import OpProgress
|
|
from time import time, strftime
|
|
import os
|
|
import StringIO
|
|
import string
|
|
import re
|
|
from collections import defaultdict, namedtuple
|
|
from types import StringTypes, TupleType, DictType, ListType, BooleanType
|
|
|
|
class EnvironmentConfigBroken(Exception): pass
|
|
|
|
# print environmental things
|
|
# for k,v in os.environ.iteritems(): print >> sys.stderr, "%r : %r" % (k,v)
|
|
|
|
def getEnv(name, default=None, cast=None):
|
|
"""
|
|
function to get Environmentvars, cast them and setting defaults if they aren't
|
|
getEnv('USER', default='nouser') # 'HomerS'
|
|
getEnv('WINDOWID', cast=int) # 44040201
|
|
"""
|
|
try:
|
|
var = os.environ[name]
|
|
if cast is not None:
|
|
var = cast(var)
|
|
except KeyError:
|
|
# environment does not have this var
|
|
var = default
|
|
except:
|
|
# now probably the cast went wrong
|
|
print >> sys.stderr, "for environment variable %r, %r is no valid value"%(name, var)
|
|
var = default
|
|
return var
|
|
|
|
MAX_LIST_SIZE_EXT_INFO = getEnv('MAX_LIST_SIZE_EXT_INFO', default=50, cast=int)
|
|
""" Packagelists to this size are printed as extra Information to munin """
|
|
|
|
STATE_DIR = getEnv('MUNIN_PLUGSTATE', default='.')
|
|
CACHE_FILE = os.path.join(STATE_DIR, "deb_packages.state")
|
|
"""
|
|
There is no need to execute this script every 5 minutes.
|
|
The Results are put to this file, next munin-run can read from it
|
|
CACHE_FILE is usually /var/lib/munin/plugin-state/debian_packages.state
|
|
"""
|
|
|
|
CACHE_FILE_MAX_AGE = getEnv('CACHE_FILE_MAX_AGE', default=3540, cast=int)
|
|
"""
|
|
Age in seconds an $CACHE_FILE can be. If it is older, the script updates
|
|
"""
|
|
|
|
def Property(func):
|
|
return property(**func())
|
|
|
|
class Apt(object):
|
|
"""
|
|
lazy helperclass i need in this statisticprogram, which have alle the apt_pkg stuff
|
|
"""
|
|
|
|
def __init__(self):
|
|
# init packagesystem
|
|
apt_pkg.init_config()
|
|
apt_pkg.init_system()
|
|
# NullProgress : we do not want progress info in munin plugin
|
|
# documented None did not worked
|
|
self._cache = None
|
|
self._depcache = None
|
|
self._installedPackages = None
|
|
self._upgradablePackages = None
|
|
|
|
@Property
|
|
def cache():
|
|
doc = "apt_pkg.Cache instance, lazy instantiated"
|
|
def fget(self):
|
|
class NullProgress(OpProgress):
|
|
""" used for do not giving any progress info,
|
|
while doing apt things used, cause documented
|
|
use of None as OpProgress did not worked in
|
|
python-apt 0.7
|
|
"""
|
|
def __init__(self):
|
|
self.op=''
|
|
self.percent=0
|
|
self.subop=''
|
|
|
|
def done(self):
|
|
pass
|
|
|
|
def update(*args,**kwords):
|
|
pass
|
|
|
|
if self._cache is None:
|
|
self._cache = apt_pkg.Cache(NullProgress())
|
|
return self._cache
|
|
return locals()
|
|
|
|
@Property
|
|
def depcache():
|
|
doc = "apt_pkg.DepCache object"
|
|
|
|
def fget(self):
|
|
if self._depcache is None:
|
|
self._depcache = apt_pkg.DepCache(self.cache)
|
|
return self._depcache
|
|
|
|
return locals()
|
|
|
|
@Property
|
|
def installedPackages():
|
|
doc = """apt_pkg.PackageList with installed Packages
|
|
it is a simple ListType with Elements of apt_pkg.Package
|
|
"""
|
|
|
|
def fget(self):
|
|
""" returns a apt_pkg.PackageList with installed Packages
|
|
it is a simple ListType with Elements of apt_pkg.Package
|
|
"""
|
|
if self._installedPackages is None:
|
|
self._installedPackages = []
|
|
for p in self.cache.packages:
|
|
if not ( p.current_state == apt_pkg.CURSTATE_NOT_INSTALLED or
|
|
p.current_state == apt_pkg.CURSTATE_CONFIG_FILES ):
|
|
self._installedPackages.append(p)
|
|
return self._installedPackages
|
|
|
|
return locals()
|
|
|
|
@Property
|
|
def upgradablePackages():
|
|
|
|
doc = """apt_pkg.PackageList with Packages that are upgradable
|
|
it is a simple ListType with Elements of apt_pkg.Package
|
|
"""
|
|
|
|
def fget(self):
|
|
if self._upgradablePackages is None:
|
|
self._upgradablePackages = []
|
|
for p in self.installedPackages:
|
|
if self.depcache.is_upgradable(p):
|
|
self._upgradablePackages.append(p)
|
|
return self._upgradablePackages
|
|
|
|
return locals()
|
|
|
|
apt = Apt()
|
|
""" global instance of apt data, used here
|
|
|
|
apt.cache
|
|
apt.depcache
|
|
apt.installedPackages
|
|
apt.upgradablePackages
|
|
|
|
initialisation is lazy
|
|
"""
|
|
|
|
def weightOfPackageFile(detail_tuple, option_tuple):
|
|
"""
|
|
calculates a weight, you can sort with
|
|
if detail_tuple is: ['label', 'archive']
|
|
option_tuple is: ['Debian', 'unstable']
|
|
it calculates
|
|
sortDict['label']['Debian'] * multiplierDict['label']
|
|
+ sortDict['archive']['unstable'] * multiplierDict['archive']
|
|
= 10 * 10**4 + 50 * 10**8
|
|
= 5000100000
|
|
"""
|
|
val = 0L
|
|
for option, detail in zip(option_tuple, detail_tuple):
|
|
optionValue = PackageStat.sortDict[option][detail]
|
|
val += optionValue * PackageStat.multiplierDict[option]
|
|
return val
|
|
|
|
def Tree():
|
|
""" Tree type generator
|
|
you can put data at the end of a twig
|
|
a = Tree()
|
|
a['a']['b']['c'] # creates the tree of depth 3
|
|
a['a']['b']['d'] # creates another twig of the tree
|
|
c
|
|
a — b <
|
|
d
|
|
"""
|
|
return TreeTwig(Tree)
|
|
|
|
class TreeTwig(defaultdict):
|
|
def __init__(self, defaultFactory):
|
|
super(TreeTwig, self).__init__(defaultFactory)
|
|
|
|
def printAsTree(self, indent=0):
|
|
for k, tree in self.iteritems():
|
|
print " " * indent, repr(k)
|
|
if isinstance(tree, TreeTwig):
|
|
printTree(tree, indent+1)
|
|
else:
|
|
print tree
|
|
|
|
def printAsLine(self):
|
|
print self.asLine()
|
|
|
|
def asLine(self):
|
|
values = ""
|
|
for key, residue in self.iteritems():
|
|
if residue:
|
|
values += " %r" % key
|
|
if isinstance(residue, TreeTwig):
|
|
if len(residue) == 1:
|
|
values += " - %s" % residue.asLine()
|
|
else:
|
|
values += "(%s)" % residue.asLine()
|
|
else:
|
|
values += "(%s)" % residue
|
|
else:
|
|
values += " %r," % key
|
|
return values.strip(' ,')
|
|
|
|
|
|
def getShortestConfigOfOptions(optionList = ['label', 'archive', 'site']):
|
|
"""
|
|
tries to find the order to print a tree of the optionList
|
|
with the local repositories with the shortest line
|
|
possible options are:
|
|
'component'
|
|
'label'
|
|
'site'
|
|
'archive'
|
|
'origin'
|
|
'architecture'
|
|
Architecture values are usually the same and can be ignored.
|
|
|
|
tells you which representation of a tree as line is shortest.
|
|
Is needed to say which ext.info line would be the shortest
|
|
to write the shortest readable output.
|
|
"""
|
|
l = optionList # just because l is much shorter
|
|
|
|
# creating possible iterations
|
|
fieldCount = len(optionList)
|
|
if fieldCount == 1:
|
|
selection = l
|
|
elif fieldCount == 2:
|
|
selection = [(x,y)
|
|
for x in l
|
|
for y in l if x!=y ]
|
|
elif fieldCount == 3:
|
|
selection = [(x,y,z)
|
|
for x in l
|
|
for y in l if x!=y
|
|
for z in l if z!=y and z!=x]
|
|
else:
|
|
raise Exception("NotImplemented for size %s" % fieldCount)
|
|
|
|
# creating OptionsTree, and measuring the length of it on a line
|
|
# for every iteration
|
|
d = {}
|
|
for keys in selection:
|
|
d[keys] = len( getOptionsTree(apt.cache, keys).asLine() )
|
|
|
|
# finding the shortest variant
|
|
r = min( d.items(), key=lambda x: x[1] )
|
|
|
|
return list(r[0]), r[1]
|
|
|
|
def getOptionsTree(cache, keys=None):
|
|
"""
|
|
t = getOptionsTree(cache, ['archive', 'site', 'label'])
|
|
generates ad dict of dict of sets like:
|
|
...
|
|
it tells you:
|
|
...
|
|
"""
|
|
t = Tree()
|
|
for f in cache.file_list:
|
|
# ignoring translation indexes ...
|
|
if f.index_type != 'Debian Package Index' and f.index_type !='Debian dpkg status file':
|
|
continue
|
|
# ignoring files with 0 size
|
|
if f.size == 0L:
|
|
continue
|
|
# creating default dict in case of secondary_options are empty
|
|
d = t
|
|
for key in keys:
|
|
if not key:
|
|
print f
|
|
dKey = f.__getattribute__(key)
|
|
d = d[dKey]
|
|
return t
|
|
|
|
def createKey(key, file):
|
|
"""
|
|
createKey( (archive, origin), apt.pkg_file)
|
|
returns ('unstable', 'Debian')
|
|
"""
|
|
if type(key) in StringTypes:
|
|
return file.__getattribute__(key)
|
|
elif type(key) in (TupleType, ListType):
|
|
nKey = tuple()
|
|
for pKey in key:
|
|
nKey = nKey.__add__((file.__getattribute__(pKey),))
|
|
return nKey
|
|
else:
|
|
raise Exception("Not implemented for keytype %s" % type(key))
|
|
|
|
def getOptionsTree2(cache, primary=None, secondary=None):
|
|
"""
|
|
primary muss ein iterable oder StringType sein
|
|
secondary muss iterable oder StringType sein
|
|
t1 = getOptionsTree2(apt.cache, 'origin', ['site', 'archive'])
|
|
t2 = getOptionsTree2(apt.cache, ['origin', 'archive'], ['site', 'label'])
|
|
"""
|
|
|
|
|
|
if type(secondary) in StringTypes:
|
|
secondary = [secondary]
|
|
if type(primary) in StringTypes:
|
|
primary = [primary]
|
|
|
|
t = Tree()
|
|
for file in cache.file_list:
|
|
# ignoring translation indexes ...
|
|
if file.index_type not in ['Debian Package Index', 'Debian dpkg status file']:
|
|
continue
|
|
# ignoring files with 0 size
|
|
if file.size == 0L:
|
|
continue
|
|
|
|
# key to first Dict in Tree is a tuple
|
|
pKey = createKey(primary, file)
|
|
d = t[pKey]
|
|
if secondary is not None:
|
|
# for no, sKey in enumerate(secondary):
|
|
# dKey = file.__getattribute__(sKey)
|
|
# if no < len(secondary)-1:
|
|
# d = d[dKey]
|
|
# if isinstance(d[dKey], DictType):
|
|
# d[dKey] = []
|
|
# d[dKey].append(file)
|
|
|
|
for sKey in secondary:
|
|
dKey = file.__getattribute__(sKey)
|
|
d = d[dKey]
|
|
return t
|
|
|
|
#def getAttributeSet(iterable, attribute):
|
|
# return set(f.__getattribute__(attribute) for f in iterable)
|
|
#
|
|
#def getOrigins(cache):
|
|
# return getAttributeSet(cache.file_list, 'origin')
|
|
#
|
|
#def getArchives(cache):
|
|
# return getAttributeSet(cache.file_list, 'archive')
|
|
#
|
|
#def getComponents(cache):
|
|
# return getAttributeSet(cache.file_list, 'component')
|
|
#
|
|
#def getLabels(cache):
|
|
# return getAttributeSet(cache.file_list, 'label')
|
|
#
|
|
#def getSites(cache):
|
|
# return getAttributeSet(cache.file_list, 'site')
|
|
#
|
|
|
|
class PackageStat(defaultdict):
|
|
""" defaultdict with Tuple Keys of (label,archive) containing lists of ArchiveFiles
|
|
{('Debian Backports', 'squeeze-backports'): [...]
|
|
('The Opera web browser', 'oldstable'): [...]
|
|
('Debian', 'unstable'): [...]}
|
|
with some abilities to print output munin likes
|
|
"""
|
|
|
|
sortDict = { 'label': defaultdict( lambda : 20,
|
|
{'Debian': 90,
|
|
'' : 1,
|
|
'Debian Security' : 90,
|
|
'Debian Backports': 90}),
|
|
'archive': defaultdict( lambda : 5,
|
|
{ 'now': 0,
|
|
'experimental': 10,
|
|
'unstable': 50,
|
|
'sid': 50,
|
|
'testing': 70,
|
|
'wheezy': 70,
|
|
'squeeze-backports': 80,
|
|
'stable-backports': 80,
|
|
'proposed-updates': 84,
|
|
'stable-updates': 85,
|
|
'stable': 90,
|
|
'squeeze': 90,
|
|
'oldstable': 95,
|
|
'lenny': 95, } ),
|
|
'site': defaultdict( lambda : 5, { }),
|
|
'origin': defaultdict( lambda : 5, { 'Debian' : 90, }),
|
|
'component': defaultdict( lambda : 5, {
|
|
'non-free': 10,
|
|
'contrib' : 50,
|
|
'main' : 90, }),
|
|
}
|
|
"""
|
|
Values to sort options (label, archive, origin ...)
|
|
(0..99) is allowed.
|
|
(this is needed for other graphs to calc aggregated weights)
|
|
higher is more older and more official or better
|
|
"""
|
|
|
|
dpkgStatusValue = { 'site': '', 'origin': '', 'label': '', 'component': '', 'archive': 'now' }
|
|
""" a dict to recognize options that coming from 'Debian dpkg status file' """
|
|
|
|
viewSet = set(['label', 'archive', 'origin', 'site', 'component'])
|
|
|
|
multiplierDict = { 'label' : 10**8,
|
|
'archive' : 10**4,
|
|
'site' : 10**0,
|
|
'origin' : 10**6,
|
|
'component' : 10**2,
|
|
}
|
|
"""
|
|
Dict that stores multipliers
|
|
to compile a sorting value for each archivefile
|
|
"""
|
|
|
|
def weight(self, detail_tuple):
|
|
return weightOfPackageFile(detail_tuple=detail_tuple, option_tuple=tuple(self.option))
|
|
|
|
def __init__(self, packetHandler, apt=apt, sortBy=None, extInfo=None, includeNow=True, *args, **kwargs):
|
|
assert isinstance(packetHandler, PacketHandler)
|
|
self.packetHandler = packetHandler
|
|
self.apt = apt
|
|
self.option = sortBy if sortBy is not None else ['label', 'archive']
|
|
optionsMentionedInExtInfo = extInfo if extInfo is not None else list(self.viewSet - set(self.option))
|
|
self.options = getOptionsTree2(apt.cache, self.option, optionsMentionedInExtInfo)
|
|
self.options_sorted = self._sorted(self.options.items())
|
|
super(PackageStat, self).__init__(lambda: [], *args, **kwargs)
|
|
|
|
translationTable = string.maketrans(' -.', '___')
|
|
""" chars that must not exist in a munin system name"""
|
|
|
|
@classmethod
|
|
def generate_rrd_name_from(cls, string):
|
|
return string.translate(cls.translationTable)
|
|
|
|
def _sorted(self, key_value_pairs):
|
|
return sorted(key_value_pairs, key=lambda(x): self.weight(x[0]), reverse=True)
|
|
|
|
@classmethod
|
|
def generate_rrd_name_from(cls, keyTuple):
|
|
assert isinstance(keyTuple, TupleType) or isinstance(keyTuple, ListType)
|
|
# we have to check, whether all tuple-elements have values
|
|
l = []
|
|
for key in keyTuple:
|
|
key = key if key else "local"
|
|
l.append(key)
|
|
return string.join(l).lower().translate(cls.translationTable)
|
|
|
|
def addPackage(self, sourceFile, package):
|
|
if self.packetHandler.decider(package):
|
|
self.packetHandler.adder(package, self)
|
|
|
|
@classmethod
|
|
def configD(cls, key, value):
|
|
i = { 'rrdName': cls.generate_rrd_name_from(key),
|
|
'options': string.join(key,'/'),
|
|
'info' : "from %r" % value.asLine() }
|
|
return i
|
|
|
|
def configHead(self):
|
|
d = { 'graphName': "packages_"+ self.generate_rrd_name_from(self.option),
|
|
'option': string.join(self.option, '/'),
|
|
'type' : self.packetHandler.type
|
|
}
|
|
return "\n"\
|
|
"multigraph {graphName}_{type}\n"\
|
|
"graph_title {type} Debian packages sorted by {option}\n"\
|
|
"graph_info {type} Debian packages sorted by {option} of its repository\n"\
|
|
"graph_category security\n"\
|
|
"graph_vlabel packages".format(**d)
|
|
|
|
def printConfig(self):
|
|
print self.configHead()
|
|
for options, item in self.options_sorted:
|
|
if not self.packetHandler.includeNow and self.optionIsDpkgStatus(details=options):
|
|
continue
|
|
i = self.configD(options, item)
|
|
print "{rrdName}.label {options}".format(**i)
|
|
print "{rrdName}.info {info}".format(**i)
|
|
print "{rrdName}.draw AREASTACK".format(**i)
|
|
|
|
def optionIsDpkgStatus(self, details, options=None):
|
|
"""
|
|
give it details and options and it tells you whether the datails looks like they come from
|
|
a 'Debian dpkg status file'.
|
|
"""
|
|
# setting defaults
|
|
if options is None:
|
|
options = self.option
|
|
assert type(details) in (TupleType, ListType), 'details must be tuple or list not %r' % type(details)
|
|
assert type(options) in (TupleType, ListType), 'options must be tuple or list not %r' % type(details)
|
|
assert len(details) == len(options)
|
|
isNow = True
|
|
for det, opt in zip(details, options):
|
|
isNow &= self.dpkgStatusValue[opt] == det
|
|
return isNow
|
|
|
|
def printValues(self):
|
|
print "\nmultigraph packages_{option}_{type}".format(option=self.generate_rrd_name_from(self.option),
|
|
type=self.packetHandler.type)
|
|
for options, item in self.options_sorted:
|
|
if not self.packetHandler.includeNow and self.optionIsDpkgStatus(details=options):
|
|
continue
|
|
i = self.configD(options, item)
|
|
i['value'] = len(self.get(options, []))
|
|
print "{rrdName}.value {value}".format(**i)
|
|
self._printExtInfoPackageList(options)
|
|
|
|
def _printExtInfoPackageList(self, options):
|
|
rrdName = self.generate_rrd_name_from(options)
|
|
packageList = self[options]
|
|
packageCount = len( packageList )
|
|
if 0 < packageCount <= MAX_LIST_SIZE_EXT_INFO:
|
|
print "%s.extinfo " % rrdName,
|
|
for item in packageList:
|
|
print self.packetHandler.extInfoItemString.format(i=item),
|
|
print
|
|
|
|
packetHandlerD = {}
|
|
""" Dictionary for PacketHandlerclasses with its 'type'-key """
|
|
|
|
class PacketHandler(object):
|
|
"""
|
|
Baseclass, that represents the Interface which is used
|
|
"""
|
|
|
|
type = None
|
|
includeNow = None
|
|
extInfoItemString = None
|
|
|
|
def __init__(self, apt):
|
|
self.apt = apt
|
|
|
|
def decider(self, package, *args, **kwords):
|
|
"""
|
|
Function works as decider
|
|
if it returns True, the package is added
|
|
if it returns False, the package is not added
|
|
"""
|
|
pass
|
|
|
|
def adder(self, package, packageStat, *args, **kwords):
|
|
"""
|
|
take the package and add it tho the packageStat dictionary in defined way
|
|
"""
|
|
pass
|
|
|
|
@classmethod
|
|
def keyOf(cls, pFile):
|
|
"""
|
|
calculates the weight of a apt_pkg.PackageFile
|
|
"""
|
|
options = ('origin', 'site', 'archive', 'component', 'label')
|
|
details = tuple()
|
|
for option in options:
|
|
details = details.__add__((pFile.__getattribute__(option),))
|
|
return weightOfPackageFile(details, options)
|
|
|
|
class PacketHandlerUpgradable(PacketHandler):
|
|
|
|
type='upgradable'
|
|
includeNow = False
|
|
extInfoItemString = " {i[0].name} <{i[1]} -> {i[2]}>"
|
|
|
|
def decider(self, package, *args, **kwords):
|
|
return self.apt.depcache.is_upgradable(package)
|
|
|
|
def adder(self, package, packageStat, *args, **kwords):
|
|
options = tuple(packageStat.option)
|
|
candidateP = self.apt.depcache.get_candidate_ver(package)
|
|
candidateFile = max(candidateP.file_list, key=lambda f: self.keyOf(f[0]) )[0]
|
|
keys = createKey(options, candidateFile)
|
|
# this item (as i) is used for input in extInfoItemString
|
|
item = (package, package.current_ver.ver_str, candidateP.ver_str)
|
|
packageStat[keys].append(item)
|
|
|
|
# registering PackageHandler for Usage
|
|
packetHandlerD[PacketHandlerUpgradable.type] = PacketHandlerUpgradable
|
|
|
|
class PacketHandlerInstalled(PacketHandler):
|
|
type = 'installed'
|
|
includeNow = True
|
|
extInfoItemString = " {i.name}"
|
|
|
|
def decider(self, package, *args, **kwords):
|
|
# this function is called with each installed package
|
|
return True
|
|
|
|
def adder(self, package, packageStat, *args, **kwords):
|
|
options = tuple(packageStat.option)
|
|
candidateP = self.apt.depcache.get_candidate_ver(package)
|
|
candidateFile = max(candidateP.file_list, key=lambda f: self.keyOf(f[0]) )[0]
|
|
keys = createKey(options, candidateFile)
|
|
# this item (as i) is used for input in extInfoItemString
|
|
item = package
|
|
packageStat[keys].append(item)
|
|
|
|
# registering PackageHandler for Usage
|
|
packetHandlerD[PacketHandlerInstalled.type] = PacketHandlerInstalled
|
|
|
|
class Munin(object):
|
|
|
|
def __init__(self, commandLineArgs=None):
|
|
self.commandLineArgs = commandLineArgs
|
|
self.argParser = self._argParser()
|
|
self.executionMatrix = {
|
|
'config': self.config,
|
|
'run' : self.run,
|
|
'autoconf' : self.autoconf,
|
|
}
|
|
self.envConfig = self._envParser()
|
|
self._envValidater()
|
|
# print >> sys.stderr, self.envConfig
|
|
self.statL = []
|
|
if self.envConfig:
|
|
for config in self.envConfig:
|
|
packetHandler = packetHandlerD[config['type']](apt)
|
|
packageStat = PackageStat(apt=apt,
|
|
packetHandler = packetHandler,
|
|
sortBy = config['sort_by'],
|
|
extInfo = config['show_ext'])
|
|
self.statL.append(packageStat)
|
|
if not self.statL:
|
|
print "# no munin config found in environment vars"
|
|
|
|
def execute(self):
|
|
self.args = self.argParser.parse_args(self.commandLineArgs)
|
|
self.executionMatrix[self.args.command]()
|
|
|
|
def _cacheIsOutdated(self):
|
|
"""
|
|
# interesting files are pkgcache.bin (if it exists (it is deleted after apt-get clean))
|
|
# if a file is intstalled or upgraded, '/var/lib/dpkg/status' is changed
|
|
"""
|
|
if os.path.isfile(CACHE_FILE):
|
|
cacheMTime = os.stat(CACHE_FILE).st_mtime
|
|
else:
|
|
# no cachestatus file exist, so it _must_ renewed
|
|
return True
|
|
# List of modify-times of different files
|
|
timeL = []
|
|
packageListsDir = "/var/lib/apt/lists"
|
|
files=os.listdir(packageListsDir)
|
|
packageFileL = [ file for file in files if file.endswith('Packages')]
|
|
for packageFile in packageFileL:
|
|
timeL.append(os.stat(os.path.join(packageListsDir, packageFile)).st_mtime)
|
|
|
|
dpkgStatusFile = '/var/lib/dpkg/status'
|
|
if os.path.isfile(dpkgStatusFile):
|
|
timeL.append(os.stat(dpkgStatusFile).st_mtime)
|
|
else:
|
|
raise Exception('DPKG-statusfile %r not found, really strange!!!'%dpkgStatusFile)
|
|
newestFileTimestamp = max(timeL)
|
|
age = newestFileTimestamp - cacheMTime
|
|
if age > 0:
|
|
return True
|
|
else:
|
|
# if we have made a timetravel, we update until we reached good times
|
|
if time() < newestFileTimestamp:
|
|
return True
|
|
return False
|
|
|
|
def _run_with_cache(self):
|
|
""" wrapper around _run with writing to file and stdout
|
|
a better way would be a 'shell' tee as stdout
|
|
"""
|
|
# cacheNeedUpdate = False
|
|
# if not self.args.nocache:
|
|
# # check, whether the cachefile has to be written again
|
|
# if os.path.isfile(CACHE_FILE):
|
|
# mtime = os.stat(CACHE_FILE).st_mtime
|
|
# age = time() - mtime
|
|
# cacheNeedUpdate = age < 0 or age > CACHE_FILE_MAX_AGE
|
|
# else:
|
|
# cacheNeedUpdate = True
|
|
|
|
if self._cacheIsOutdated() or self.args.nocache:
|
|
# save stdout
|
|
stdoutDef = sys.stdout
|
|
try:
|
|
out = StringIO.StringIO()
|
|
sys.stdout = out
|
|
# run writes now to new sys.stdout
|
|
print "# executed at %r (%r)" %(strftime("%s"), strftime("%c"))
|
|
self._run()
|
|
sys.stdout = stdoutDef
|
|
# print output to stdout
|
|
stdoutDef.write(out.getvalue())
|
|
# print output to CACHE_FILE
|
|
with open(CACHE_FILE,'w') as state:
|
|
state.write(out.getvalue())
|
|
except IOError as e:
|
|
if e.errno == 2:
|
|
sys.stderr.write("%s : %s" % (e.msg, CACHE_FILE))
|
|
# 'No such file or directory'
|
|
os.makedirs( os.path.dirname(CACHE_FILE) )
|
|
else:
|
|
print sys.stderr.write("%r : %r" % (e, CACHE_FILE))
|
|
finally:
|
|
# restore stdout
|
|
sys.stdout = stdoutDef
|
|
else:
|
|
with open(CACHE_FILE,'r') as data:
|
|
print data.read()
|
|
|
|
def _run(self):
|
|
# p … package
|
|
# do the real work
|
|
for p in apt.installedPackages:
|
|
sourceFile = max(p.current_ver.file_list, key=lambda f: PacketHandler.keyOf(f[0]) )[0]
|
|
for packageStat in self.statL:
|
|
packageStat.addPackage(sourceFile, p)
|
|
|
|
# print munin output
|
|
for stat in self.statL:
|
|
stat.printValues()
|
|
|
|
def run(self):
|
|
if self.args.nocache:
|
|
self._run()
|
|
else:
|
|
self._run_with_cache()
|
|
|
|
def config(self):
|
|
for stat in self.statL:
|
|
stat.printConfig()
|
|
|
|
def autoconf(self):
|
|
print 'yes'
|
|
|
|
def _argParser(self):
|
|
parser = argparse.ArgumentParser(description="Show some statistics "\
|
|
"about debian packages installed on system by archive",
|
|
)
|
|
parser.set_defaults(command='run', debug=True, nocache=True)
|
|
|
|
parser.add_argument('--nocache', '-n', default=False, action='store_true',
|
|
help='do not use a cache file')
|
|
helpCommand = """
|
|
config ..... writes munin config
|
|
run ........ munin run (writes values)
|
|
autoconf ... writes 'yes'
|
|
"""
|
|
parser.add_argument('command', nargs='?',
|
|
choices=['config', 'run', 'autoconf', 'drun'],
|
|
help='mode munin wants to use. "run" is default' + helpCommand)
|
|
return parser
|
|
|
|
def _envParser(self):
|
|
"""
|
|
reads environVars from [deb_packages] and generate
|
|
a list of dicts, each dict holds a set of settings made in
|
|
munin config.
|
|
[
|
|
{ 'type' = 'installed',
|
|
'sort_by' = ['label', 'archive'],
|
|
'show_ext' = ['origin', 'site'],
|
|
},
|
|
{ 'type' = 'upgraded',
|
|
'sort_by' = ['label', 'archive'],
|
|
'show_ext' = ['origin', 'site'],
|
|
}
|
|
]
|
|
"""
|
|
def configStartDict():
|
|
return { 'type': None,
|
|
'sort_by': dict(),
|
|
'show_ext' : dict(),
|
|
}
|
|
|
|
interestingVarNameL = [ var for var in os.environ if var.startswith('graph') ]
|
|
config = defaultdict(configStartDict)
|
|
regex = re.compile(r"graph(?P<graphNumber>\d+)_(?P<res>.*?)_?(?P<optNumber>\d+)?$")
|
|
for var in interestingVarNameL:
|
|
m = re.match(regex, var)
|
|
configPart = config[m.group('graphNumber')]
|
|
if m.group('res') == 'type':
|
|
configPart['type'] = os.getenv(var)
|
|
elif m.group('res') == 'sort_by':
|
|
configPart['sort_by'][m.group('optNumber')] = os.getenv(var)
|
|
elif m.group('res') == 'show_ext':
|
|
configPart['show_ext'][m.group('optNumber')] = os.getenv(var)
|
|
else:
|
|
print >> sys.stderr, "configuration option %r was ignored" % (var)
|
|
# we have now dicts for 'sort_by' and 'show_ext' keys
|
|
# changing them to lists
|
|
for graphConfig in config.itervalues():
|
|
graphConfig['sort_by'] = [val for key, val in sorted(graphConfig['sort_by'].items())]
|
|
graphConfig['show_ext'] = [val for key, val in sorted(graphConfig['show_ext'].items())]
|
|
# we do not want keynames, they are only needed for sorting environmentvars
|
|
return [val for key, val in sorted(config.items())]
|
|
|
|
def _envValidater(self):
|
|
""" takes the munin config and checks for valid configuration,
|
|
raises Exception if something is broken
|
|
"""
|
|
for graph in self.envConfig:
|
|
if graph['type'] not in ('installed', 'upgradable'):
|
|
print >> sys.stderr, \
|
|
"GraphType must be 'installed' or 'upgradable' but not %r"%(graph.type), \
|
|
graph
|
|
raise EnvironmentConfigBroken("Environment Config broken")
|
|
if not graph['sort_by']:
|
|
print >> sys.stderr, \
|
|
"Graph must be sorted by anything"
|
|
raise EnvironmentConfigBroken("Environment Config broken")
|
|
# check for valid options for sort_by
|
|
unusableOptions = set(graph['sort_by']) - PackageStat.viewSet
|
|
if unusableOptions:
|
|
print >> sys.stderr, \
|
|
"%r are not valid options for 'sort_by'" % (unusableOptions)
|
|
raise EnvironmentConfigBroken("Environment Config broken")
|
|
# check for valid options for sort_by
|
|
unusableOptions = set(graph['show_ext']) - PackageStat.viewSet
|
|
if unusableOptions:
|
|
print >> sys.stderr, \
|
|
"%r are not valid options for 'show_ext'" % (x)
|
|
raise EnvironmentConfigBroken("Environment Config broken")
|
|
|
|
if __name__=='__main__':
|
|
muninPlugin = Munin()
|
|
muninPlugin.execute()
|
|
# import IPython; IPython.embed()
|
|
|
|
|
|
### The following is the smart_ plugin documentation, intended to be used with munindoc
|
|
|
|
"""
|
|
=head1 NAME
|
|
|
|
deb_packages - plugin to monitor update resources and pending packages on Debian
|
|
|
|
=head1 APPLICABLE SYSTEMS
|
|
|
|
This plugin has checked on Debian - Wheezy and squeeze. If you want to use it
|
|
on older installations, tell me whether it works or which errors you had. It
|
|
shoud run past python-apt 0.7 and python 2.5.
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
With this plugin munin can give you a nice graph and some details where your
|
|
packages come from, how old or new your installation is. Furtermore it tells
|
|
you how many updates you should have been installed, how many packages are
|
|
outdated and where they come from.
|
|
|
|
You can sort installed or upgradable Packages by 'archive', 'origin', 'site',
|
|
'label' and 'component' and even some of them at once.
|
|
|
|
The script uses caching cause it is quite expensive. It saves the output to a
|
|
cachefile and checks on each run, if dpkg-status or downloaded Packagefile have
|
|
changed. If one of them has changed, it runs, if not it gives you the cached
|
|
version
|
|
|
|
=head1 INSTALLATION
|
|
|
|
check out this git repository from
|
|
|
|
=over 2
|
|
|
|
aptitude install python-apt
|
|
git clone git://github.com/munin-monitoring/contrib.git
|
|
cd contrib/plugins/apt/deb_packages
|
|
sudo cp deb_packages.py /etc/munin/plugins/deb_packages
|
|
sudo cp deb_packages.munin.conf /etc/munin/plugin-conf.d/deb_packages
|
|
|
|
=back
|
|
|
|
Verify the installation by
|
|
|
|
=over 2
|
|
|
|
sudo munin-run deb_packages
|
|
|
|
=back
|
|
|
|
|
|
=head1 CONFIGURATION
|
|
|
|
If you copied deb_packages.munin.conf to plugin-conf.d you have a starting point.
|
|
|
|
A typical configuration looks like this
|
|
|
|
=over 2
|
|
|
|
[deb_packages]
|
|
# plugin is quite expensive and has to write statistics to cache output
|
|
# so it has to write to plugins.cache
|
|
user munin
|
|
|
|
# Packagelists to this size are printed as extra information to munin.extinfo
|
|
env.MAX_LIST_SIZE_EXT_INFO 50
|
|
|
|
# Age in seconds an $CACHE_FILE can be. If it is older, the script updates
|
|
# default if not set is 3540 (one hour)
|
|
# at the moment this is not used, the plugin always runs (if munin calls it)
|
|
#
|
|
env.CACHE_FILE_MAX_AGE 3540
|
|
|
|
# All these numbers are only for sorting, so you can use env.graph01_sort_by_0
|
|
# and env.graph01_sort_by_2 without using env.graph01_sort_by_1.
|
|
# sort_by values ...
|
|
# possible values are 'label', 'archive', 'origin', 'site', 'component'
|
|
env.graph00_type installed
|
|
env.graph00_sort_by_0 label
|
|
env.graph00_sort_by_1 archive
|
|
env.graph00_show_ext_0 origin
|
|
env.graph00_show_ext_1 site
|
|
|
|
env.graph01_type upgradable
|
|
env.graph01_sort_by_0 label
|
|
env.graph01_sort_by_1 archive
|
|
env.graph01_show_ext_0 origin
|
|
env.graph01_show_ext_1 site
|
|
|
|
=back
|
|
|
|
You can sort_by one or some of these possible Values
|
|
|
|
|
|
=head1 AUTHOR
|
|
|
|
unknown
|
|
|
|
=head1 LICENSE
|
|
|
|
Default for Munin contributions is GPLv2 (http://www.gnu.org/licenses/gpl-2.0.txt)
|
|
|
|
=cut
|
|
|
|
|
|
"""
|