From b64fa21883c61143b3143f353edfa7e0b5f4a4ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20L=C3=A4ssig?= Date: Wed, 2 May 2012 17:44:58 +0200 Subject: [PATCH] added apt debian packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Björn Lässig --- plugins/apt/deb_packages/.gitignore | 27 + plugins/apt/deb_packages/README.md | 69 ++ .../apt/deb_packages/deb_packages.munin-conf | 26 + plugins/apt/deb_packages/deb_packages.py | 857 ++++++++++++++++++ ...packages_label_archive_upgradable-week.png | Bin 0 -> 22153 bytes 5 files changed, 979 insertions(+) create mode 100644 plugins/apt/deb_packages/.gitignore create mode 100644 plugins/apt/deb_packages/README.md create mode 100644 plugins/apt/deb_packages/deb_packages.munin-conf create mode 100755 plugins/apt/deb_packages/deb_packages.py create mode 100644 plugins/apt/deb_packages/example/packages_label_archive_upgradable-week.png diff --git a/plugins/apt/deb_packages/.gitignore b/plugins/apt/deb_packages/.gitignore new file mode 100644 index 00000000..f24cd995 --- /dev/null +++ b/plugins/apt/deb_packages/.gitignore @@ -0,0 +1,27 @@ +*.py[co] + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg diff --git a/plugins/apt/deb_packages/README.md b/plugins/apt/deb_packages/README.md new file mode 100644 index 00000000..43046764 --- /dev/null +++ b/plugins/apt/deb_packages/README.md @@ -0,0 +1,69 @@ +munin-debian-packages +===================== + +## Munin Debian Plugin + +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. + +![A week of upgradable packages](/Farom/munin-debian-packages/raw/master/example/packages_label_archive_upgradable-week.png) + +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. + +### Installation + +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. + +check out this git repository from + + aptitude install python-apt + git clone git://github.com/Farom/munin-debian-packages.git + cd munin-debian-packages + sudo cp deb_packages.py /etc/munin/plugins + sudo cp deb_packages.munin-conf /etc/munin/plugin-conf.d/deb_packages + +### Configuration +If you copied deb_packages.munin-conf to plugin-conf.d you have a starting point. +A typical configuration looks like this + + [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 + +You can sort_by one or some of these possible Values diff --git a/plugins/apt/deb_packages/deb_packages.munin-conf b/plugins/apt/deb_packages/deb_packages.munin-conf new file mode 100644 index 00000000..79f42d63 --- /dev/null +++ b/plugins/apt/deb_packages/deb_packages.munin-conf @@ -0,0 +1,26 @@ +[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) +env.CACHE_FILE_MAX_AGE 3540 + +# sort_by values ... +# possible values are 'label', 'archive', 'origin', 'site', FIXME +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 + diff --git a/plugins/apt/deb_packages/deb_packages.py b/plugins/apt/deb_packages/deb_packages.py new file mode 100755 index 00000000..f0c37e11 --- /dev/null +++ b/plugins/apt/deb_packages/deb_packages.py @@ -0,0 +1,857 @@ +#!/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 dependancies, + 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 wich 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 debian\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\d+)_(?P.*?)_?(?P\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() diff --git a/plugins/apt/deb_packages/example/packages_label_archive_upgradable-week.png b/plugins/apt/deb_packages/example/packages_label_archive_upgradable-week.png new file mode 100644 index 0000000000000000000000000000000000000000..4c2515d0b8ba1ef36285f5c6926d7a6d9293ec10 GIT binary patch literal 22153 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 eg*>U#jck-;