root / plugins / apt / deb_packages / deb_packages.py @ 8589c6df
Historique | Voir | Annoter | Télécharger (34 ko)
| 1 | b64fa218 | Björn Lässig | #!/usr/bin/python
|
|---|---|---|---|
| 2 | # -*- coding: utf-8 -*-
|
||
| 3 | |||
| 4 | """
|
||
| 5 | A munin plugin that prints archive and their upgradable packets
|
||
| 6 |
|
||
| 7 | TODO: make it usable and readable as commandline tool
|
||
| 8 | • (-i) interaktiv
|
||
| 9 | NICETOHAVE
|
||
| 10 | TODO: separate into 2 graphs
|
||
| 11 | • how old is my deb installation
|
||
| 12 | sorting a packet to the oldest archive
|
||
| 13 | sorting a packet to the newest archive
|
||
| 14 | (WONTFIX unless someone asks for)
|
||
| 15 |
|
||
| 16 | TODO:
|
||
| 17 | • addinge alternative names for archives "stable -> squeeze"
|
||
| 18 | TODO: add gray as
|
||
| 19 | foo.colour 000000
|
||
| 20 | to 'now', '', '', '', '', 'Debian dpkg status file'
|
||
| 21 | TODO: update only if system was updated (aptitutde update has been run)
|
||
| 22 | • check modification date of /var/cache/apt/pkgcache.bin
|
||
| 23 | • cache file must not be older than mod_date of pkgcache.bin + X
|
||
| 24 | TODO: shorten ext_info with getShortestConfigOfOptions
|
||
| 25 | TODO: check whether cachefile matches the config
|
||
| 26 | • i have no clever idea to do this without 100 lines of code
|
||
| 27 | 8589c6df | klemens | BUG: If a package will be upgraded, and brings in new dependencies,
|
| 28 | b64fa218 | Björn Lässig | these new deps will not be counted. WONTFIX
|
| 29 | """
|
||
| 30 | import sys |
||
| 31 | import argparse |
||
| 32 | import apt_pkg |
||
| 33 | from apt.progress.base import OpProgress |
||
| 34 | from time import time, strftime |
||
| 35 | import os |
||
| 36 | import StringIO |
||
| 37 | import string |
||
| 38 | import re |
||
| 39 | from collections import defaultdict, namedtuple |
||
| 40 | from types import StringTypes, TupleType, DictType, ListType, BooleanType |
||
| 41 | |||
| 42 | class EnvironmentConfigBroken(Exception): pass |
||
| 43 | |||
| 44 | # print environmental things
|
||
| 45 | # for k,v in os.environ.iteritems(): print >> sys.stderr, "%r : %r" % (k,v)
|
||
| 46 | |||
| 47 | def getEnv(name, default=None, cast=None): |
||
| 48 | """
|
||
| 49 | function to get Environmentvars, cast them and setting defaults if they aren't
|
||
| 50 | getEnv('USER', default='nouser') # 'HomerS'
|
||
| 51 | getEnv('WINDOWID', cast=int) # 44040201
|
||
| 52 | """
|
||
| 53 | try:
|
||
| 54 | var = os.environ[name] |
||
| 55 | if cast is not None: |
||
| 56 | var = cast(var) |
||
| 57 | except KeyError: |
||
| 58 | # environment does not have this var
|
||
| 59 | var = default |
||
| 60 | except:
|
||
| 61 | # now probably the cast went wrong
|
||
| 62 | print >> sys.stderr, "for environment variable %r, %r is no valid value"%(name, var) |
||
| 63 | var = default |
||
| 64 | return var
|
||
| 65 | |||
| 66 | MAX_LIST_SIZE_EXT_INFO = getEnv('MAX_LIST_SIZE_EXT_INFO', default=50, cast=int) |
||
| 67 | """ Packagelists to this size are printed as extra Information to munin """
|
||
| 68 | |||
| 69 | STATE_DIR = getEnv('MUNIN_PLUGSTATE', default='.') |
||
| 70 | CACHE_FILE = os.path.join(STATE_DIR, "deb_packages.state")
|
||
| 71 | """
|
||
| 72 | There is no need to execute this script every 5 minutes.
|
||
| 73 | The Results are put to this file, next munin-run can read from it
|
||
| 74 | CACHE_FILE is usually /var/lib/munin/plugin-state/debian_packages.state
|
||
| 75 | """
|
||
| 76 | |||
| 77 | CACHE_FILE_MAX_AGE = getEnv('CACHE_FILE_MAX_AGE', default=3540, cast=int) |
||
| 78 | """
|
||
| 79 | Age in seconds an $CACHE_FILE can be. If it is older, the script updates
|
||
| 80 | """
|
||
| 81 | |||
| 82 | def Property(func): |
||
| 83 | return property(**func()) |
||
| 84 | |||
| 85 | class Apt(object): |
||
| 86 | """
|
||
| 87 | lazy helperclass i need in this statisticprogram, which have alle the apt_pkg stuff
|
||
| 88 | """
|
||
| 89 | |||
| 90 | def __init__(self): |
||
| 91 | # init packagesystem
|
||
| 92 | apt_pkg.init_config() |
||
| 93 | apt_pkg.init_system() |
||
| 94 | # NullProgress : we do not want progress info in munin plugin
|
||
| 95 | # documented None did not worked
|
||
| 96 | self._cache = None |
||
| 97 | self._depcache = None |
||
| 98 | self._installedPackages = None |
||
| 99 | self._upgradablePackages = None |
||
| 100 | |||
| 101 | @Property
|
||
| 102 | def cache(): |
||
| 103 | doc = "apt_pkg.Cache instance, lazy instantiated"
|
||
| 104 | def fget(self): |
||
| 105 | class NullProgress(OpProgress): |
||
| 106 | """ used for do not giving any progress info,
|
||
| 107 | while doing apt things used, cause documented
|
||
| 108 | use of None as OpProgress did not worked in
|
||
| 109 | python-apt 0.7
|
||
| 110 | """
|
||
| 111 | def __init__(self): |
||
| 112 | self.op='' |
||
| 113 | self.percent=0 |
||
| 114 | self.subop='' |
||
| 115 | |||
| 116 | def done(self): |
||
| 117 | pass
|
||
| 118 | |||
| 119 | def update(*args,**kwords): |
||
| 120 | pass
|
||
| 121 | |||
| 122 | if self._cache is None: |
||
| 123 | self._cache = apt_pkg.Cache(NullProgress())
|
||
| 124 | return self._cache |
||
| 125 | return locals() |
||
| 126 | |||
| 127 | @Property
|
||
| 128 | def depcache(): |
||
| 129 | doc = "apt_pkg.DepCache object"
|
||
| 130 | |||
| 131 | def fget(self): |
||
| 132 | if self._depcache is None: |
||
| 133 | self._depcache = apt_pkg.DepCache(self.cache) |
||
| 134 | return self._depcache |
||
| 135 | |||
| 136 | return locals() |
||
| 137 | |||
| 138 | @Property
|
||
| 139 | def installedPackages(): |
||
| 140 | doc = """apt_pkg.PackageList with installed Packages
|
||
| 141 | it is a simple ListType with Elements of apt_pkg.Package
|
||
| 142 | """
|
||
| 143 | |||
| 144 | def fget(self): |
||
| 145 | """ returns a apt_pkg.PackageList with installed Packages
|
||
| 146 | it is a simple ListType with Elements of apt_pkg.Package
|
||
| 147 | """
|
||
| 148 | if self._installedPackages is None: |
||
| 149 | self._installedPackages = []
|
||
| 150 | for p in self.cache.packages: |
||
| 151 | if not ( p.current_state == apt_pkg.CURSTATE_NOT_INSTALLED or |
||
| 152 | p.current_state == apt_pkg.CURSTATE_CONFIG_FILES ): |
||
| 153 | self._installedPackages.append(p)
|
||
| 154 | return self._installedPackages |
||
| 155 | |||
| 156 | return locals() |
||
| 157 | |||
| 158 | @Property
|
||
| 159 | def upgradablePackages(): |
||
| 160 | |||
| 161 | doc = """apt_pkg.PackageList with Packages that are upgradable
|
||
| 162 | it is a simple ListType with Elements of apt_pkg.Package
|
||
| 163 | """
|
||
| 164 | |||
| 165 | def fget(self): |
||
| 166 | if self._upgradablePackages is None: |
||
| 167 | self._upgradablePackages = []
|
||
| 168 | for p in self.installedPackages: |
||
| 169 | if self.depcache.is_upgradable(p): |
||
| 170 | self._upgradablePackages.append(p)
|
||
| 171 | return self._upgradablePackages |
||
| 172 | |||
| 173 | return locals() |
||
| 174 | |||
| 175 | apt = Apt() |
||
| 176 | """ global instance of apt data, used here
|
||
| 177 |
|
||
| 178 | apt.cache
|
||
| 179 | apt.depcache
|
||
| 180 | apt.installedPackages
|
||
| 181 | apt.upgradablePackages
|
||
| 182 |
|
||
| 183 | initialisation is lazy
|
||
| 184 | """
|
||
| 185 | |||
| 186 | def weightOfPackageFile(detail_tuple, option_tuple): |
||
| 187 | """
|
||
| 188 | calculates a weight, you can sort with
|
||
| 189 | if detail_tuple is: ['label', 'archive']
|
||
| 190 | option_tuple is: ['Debian', 'unstable']
|
||
| 191 | it calculates
|
||
| 192 | sortDict['label']['Debian'] * multiplierDict['label']
|
||
| 193 | + sortDict['archive']['unstable'] * multiplierDict['archive']
|
||
| 194 | = 10 * 10**4 + 50 * 10**8
|
||
| 195 | = 5000100000
|
||
| 196 | """
|
||
| 197 | val = 0L
|
||
| 198 | for option, detail in zip(option_tuple, detail_tuple): |
||
| 199 | optionValue = PackageStat.sortDict[option][detail] |
||
| 200 | val += optionValue * PackageStat.multiplierDict[option] |
||
| 201 | return val
|
||
| 202 | |||
| 203 | def Tree(): |
||
| 204 | """ Tree type generator
|
||
| 205 | you can put data at the end of a twig
|
||
| 206 | a = Tree()
|
||
| 207 | a['a']['b']['c'] # creates the tree of depth 3
|
||
| 208 | a['a']['b']['d'] # creates another twig of the tree
|
||
| 209 | c
|
||
| 210 | a — b <
|
||
| 211 | d
|
||
| 212 | """
|
||
| 213 | return TreeTwig(Tree)
|
||
| 214 | |||
| 215 | class TreeTwig(defaultdict): |
||
| 216 | def __init__(self, defaultFactory): |
||
| 217 | super(TreeTwig, self).__init__(defaultFactory) |
||
| 218 | |||
| 219 | def printAsTree(self, indent=0): |
||
| 220 | for k, tree in self.iteritems(): |
||
| 221 | print " " * indent, repr(k) |
||
| 222 | if isinstance(tree, TreeTwig): |
||
| 223 | printTree(tree, indent+1)
|
||
| 224 | else:
|
||
| 225 | print tree
|
||
| 226 | |||
| 227 | def printAsLine(self): |
||
| 228 | print self.asLine() |
||
| 229 | |||
| 230 | def asLine(self): |
||
| 231 | values = ""
|
||
| 232 | for key, residue in self.iteritems(): |
||
| 233 | if residue:
|
||
| 234 | values += " %r" % key
|
||
| 235 | if isinstance(residue, TreeTwig): |
||
| 236 | if len(residue) == 1: |
||
| 237 | values += " - %s" % residue.asLine()
|
||
| 238 | else:
|
||
| 239 | values += "(%s)" % residue.asLine()
|
||
| 240 | else:
|
||
| 241 | values += "(%s)" % residue
|
||
| 242 | else:
|
||
| 243 | values += " %r," % key
|
||
| 244 | return values.strip(' ,') |
||
| 245 | |||
| 246 | |||
| 247 | def getShortestConfigOfOptions(optionList = ['label', 'archive', 'site']): |
||
| 248 | """
|
||
| 249 | tries to find the order to print a tree of the optionList
|
||
| 250 | with the local repositories with the shortest line
|
||
| 251 | possible options are:
|
||
| 252 | 'component'
|
||
| 253 | 'label'
|
||
| 254 | 'site'
|
||
| 255 | 'archive'
|
||
| 256 | 'origin'
|
||
| 257 | 'architecture'
|
||
| 258 | Architecture values are usually the same and can be ignored.
|
||
| 259 |
|
||
| 260 | 8589c6df | klemens | tells you which representation of a tree as line is shortest.
|
| 261 | b64fa218 | Björn Lässig | Is needed to say which ext.info line would be the shortest
|
| 262 | to write the shortest readable output.
|
||
| 263 | """
|
||
| 264 | l = optionList # just because l is much shorter
|
||
| 265 | |||
| 266 | # creating possible iterations
|
||
| 267 | fieldCount = len(optionList)
|
||
| 268 | if fieldCount == 1: |
||
| 269 | selection = l |
||
| 270 | elif fieldCount == 2: |
||
| 271 | selection = [(x,y) |
||
| 272 | for x in l |
||
| 273 | for y in l if x!=y ] |
||
| 274 | elif fieldCount == 3: |
||
| 275 | selection = [(x,y,z) |
||
| 276 | for x in l |
||
| 277 | for y in l if x!=y |
||
| 278 | for z in l if z!=y and z!=x] |
||
| 279 | else:
|
||
| 280 | raise Exception("NotImplemented for size %s" % fieldCount) |
||
| 281 | |||
| 282 | # creating OptionsTree, and measuring the length of it on a line
|
||
| 283 | # for every iteration
|
||
| 284 | d = {}
|
||
| 285 | for keys in selection: |
||
| 286 | d[keys] = len( getOptionsTree(apt.cache, keys).asLine() )
|
||
| 287 | |||
| 288 | # finding the shortest variant
|
||
| 289 | r = min( d.items(), key=lambda x: x[1] ) |
||
| 290 | |||
| 291 | return list(r[0]), r[1] |
||
| 292 | |||
| 293 | def getOptionsTree(cache, keys=None): |
||
| 294 | """
|
||
| 295 | t = getOptionsTree(cache, ['archive', 'site', 'label'])
|
||
| 296 | generates ad dict of dict of sets like:
|
||
| 297 | ...
|
||
| 298 | it tells you:
|
||
| 299 | ...
|
||
| 300 | """
|
||
| 301 | t = Tree() |
||
| 302 | for f in cache.file_list: |
||
| 303 | # ignoring translation indexes ...
|
||
| 304 | if f.index_type != 'Debian Package Index' and f.index_type !='Debian dpkg status file': |
||
| 305 | continue
|
||
| 306 | # ignoring files with 0 size
|
||
| 307 | if f.size == 0L: |
||
| 308 | continue
|
||
| 309 | # creating default dict in case of secondary_options are empty
|
||
| 310 | d = t |
||
| 311 | for key in keys: |
||
| 312 | if not key: |
||
| 313 | print f
|
||
| 314 | dKey = f.__getattribute__(key) |
||
| 315 | d = d[dKey] |
||
| 316 | return t
|
||
| 317 | |||
| 318 | def createKey(key, file): |
||
| 319 | """
|
||
| 320 | createKey( (archive, origin), apt.pkg_file)
|
||
| 321 | returns ('unstable', 'Debian')
|
||
| 322 | """
|
||
| 323 | if type(key) in StringTypes: |
||
| 324 | return file.__getattribute__(key) |
||
| 325 | elif type(key) in (TupleType, ListType): |
||
| 326 | nKey = tuple()
|
||
| 327 | for pKey in key: |
||
| 328 | nKey = nKey.__add__((file.__getattribute__(pKey),))
|
||
| 329 | return nKey
|
||
| 330 | else:
|
||
| 331 | raise Exception("Not implemented for keytype %s" % type(key)) |
||
| 332 | |||
| 333 | def getOptionsTree2(cache, primary=None, secondary=None): |
||
| 334 | """
|
||
| 335 | primary muss ein iterable oder StringType sein
|
||
| 336 | secondary muss iterable oder StringType sein
|
||
| 337 | t1 = getOptionsTree2(apt.cache, 'origin', ['site', 'archive'])
|
||
| 338 | t2 = getOptionsTree2(apt.cache, ['origin', 'archive'], ['site', 'label'])
|
||
| 339 | """
|
||
| 340 | |||
| 341 | |||
| 342 | if type(secondary) in StringTypes: |
||
| 343 | secondary = [secondary] |
||
| 344 | if type(primary) in StringTypes: |
||
| 345 | primary = [primary] |
||
| 346 | |||
| 347 | t = Tree() |
||
| 348 | for file in cache.file_list: |
||
| 349 | # ignoring translation indexes ...
|
||
| 350 | if file.index_type not in ['Debian Package Index', 'Debian dpkg status file']: |
||
| 351 | continue
|
||
| 352 | # ignoring files with 0 size
|
||
| 353 | if file.size == 0L: |
||
| 354 | continue
|
||
| 355 | |||
| 356 | # key to first Dict in Tree is a tuple
|
||
| 357 | pKey = createKey(primary, file)
|
||
| 358 | d = t[pKey] |
||
| 359 | if secondary is not None: |
||
| 360 | # for no, sKey in enumerate(secondary):
|
||
| 361 | # dKey = file.__getattribute__(sKey)
|
||
| 362 | # if no < len(secondary)-1:
|
||
| 363 | # d = d[dKey]
|
||
| 364 | # if isinstance(d[dKey], DictType):
|
||
| 365 | # d[dKey] = []
|
||
| 366 | # d[dKey].append(file)
|
||
| 367 | |||
| 368 | for sKey in secondary: |
||
| 369 | dKey = file.__getattribute__(sKey)
|
||
| 370 | d = d[dKey] |
||
| 371 | return t
|
||
| 372 | |||
| 373 | #def getAttributeSet(iterable, attribute):
|
||
| 374 | # return set(f.__getattribute__(attribute) for f in iterable)
|
||
| 375 | #
|
||
| 376 | #def getOrigins(cache):
|
||
| 377 | # return getAttributeSet(cache.file_list, 'origin')
|
||
| 378 | #
|
||
| 379 | #def getArchives(cache):
|
||
| 380 | # return getAttributeSet(cache.file_list, 'archive')
|
||
| 381 | #
|
||
| 382 | #def getComponents(cache):
|
||
| 383 | # return getAttributeSet(cache.file_list, 'component')
|
||
| 384 | #
|
||
| 385 | #def getLabels(cache):
|
||
| 386 | # return getAttributeSet(cache.file_list, 'label')
|
||
| 387 | #
|
||
| 388 | #def getSites(cache):
|
||
| 389 | # return getAttributeSet(cache.file_list, 'site')
|
||
| 390 | #
|
||
| 391 | |||
| 392 | class PackageStat(defaultdict): |
||
| 393 | """ defaultdict with Tuple Keys of (label,archive) containing lists of ArchiveFiles
|
||
| 394 | {('Debian Backports', 'squeeze-backports'): [...]
|
||
| 395 | ('The Opera web browser', 'oldstable'): [...]
|
||
| 396 | ('Debian', 'unstable'): [...]}
|
||
| 397 | with some abilities to print output munin likes
|
||
| 398 | """
|
||
| 399 | |||
| 400 | sortDict = { 'label': defaultdict( lambda : 20,
|
||
| 401 | {'Debian': 90,
|
||
| 402 | '' : 1, |
||
| 403 | 'Debian Security' : 90, |
||
| 404 | 'Debian Backports': 90}), |
||
| 405 | 'archive': defaultdict( lambda : 5, |
||
| 406 | { 'now': 0,
|
||
| 407 | 'experimental': 10, |
||
| 408 | 'unstable': 50, |
||
| 409 | 'sid': 50, |
||
| 410 | 'testing': 70, |
||
| 411 | 'wheezy': 70, |
||
| 412 | 'squeeze-backports': 80, |
||
| 413 | 'stable-backports': 80, |
||
| 414 | 'proposed-updates': 84, |
||
| 415 | 'stable-updates': 85, |
||
| 416 | 'stable': 90, |
||
| 417 | 'squeeze': 90, |
||
| 418 | 'oldstable': 95, |
||
| 419 | 'lenny': 95, } ), |
||
| 420 | 'site': defaultdict( lambda : 5, { }), |
||
| 421 | 'origin': defaultdict( lambda : 5, { 'Debian' : 90, }), |
||
| 422 | 'component': defaultdict( lambda : 5, { |
||
| 423 | 'non-free': 10, |
||
| 424 | 'contrib' : 50, |
||
| 425 | 'main' : 90, }), |
||
| 426 | } |
||
| 427 | """
|
||
| 428 | Values to sort options (label, archive, origin ...)
|
||
| 429 | (0..99) is allowed.
|
||
| 430 | (this is needed for other graphs to calc aggregated weights)
|
||
| 431 | higher is more older and more official or better
|
||
| 432 | """
|
||
| 433 | |||
| 434 | dpkgStatusValue = { 'site': '', 'origin': '', 'label': '', 'component': '', 'archive': 'now' }
|
||
| 435 | """ a dict to recognize options that coming from 'Debian dpkg status file' """
|
||
| 436 | |||
| 437 | viewSet = set(['label', 'archive', 'origin', 'site', 'component']) |
||
| 438 | |||
| 439 | multiplierDict = { 'label' : 10**8,
|
||
| 440 | 'archive' : 10**4, |
||
| 441 | 'site' : 10**0, |
||
| 442 | 'origin' : 10**6, |
||
| 443 | 'component' : 10**2, |
||
| 444 | } |
||
| 445 | """
|
||
| 446 | Dict that stores multipliers
|
||
| 447 | to compile a sorting value for each archivefile
|
||
| 448 | """
|
||
| 449 | |||
| 450 | def weight(self, detail_tuple): |
||
| 451 | return weightOfPackageFile(detail_tuple=detail_tuple, option_tuple=tuple(self.option)) |
||
| 452 | |||
| 453 | def __init__(self, packetHandler, apt=apt, sortBy=None, extInfo=None, includeNow=True, *args, **kwargs): |
||
| 454 | assert isinstance(packetHandler, PacketHandler) |
||
| 455 | self.packetHandler = packetHandler
|
||
| 456 | self.apt = apt
|
||
| 457 | self.option = sortBy if sortBy is not None else ['label', 'archive'] |
||
| 458 | optionsMentionedInExtInfo = extInfo if extInfo is not None else list(self.viewSet - set(self.option)) |
||
| 459 | self.options = getOptionsTree2(apt.cache, self.option, optionsMentionedInExtInfo) |
||
| 460 | self.options_sorted = self._sorted(self.options.items()) |
||
| 461 | super(PackageStat, self).__init__(lambda: [], *args, **kwargs) |
||
| 462 | |||
| 463 | translationTable = string.maketrans(' -.', '___') |
||
| 464 | """ chars that must not exist in a munin system name"""
|
||
| 465 | |||
| 466 | @classmethod
|
||
| 467 | def generate_rrd_name_from(cls, string): |
||
| 468 | return string.translate(cls.translationTable)
|
||
| 469 | |||
| 470 | def _sorted(self, key_value_pairs): |
||
| 471 | return sorted(key_value_pairs, key=lambda(x): self.weight(x[0]), reverse=True) |
||
| 472 | |||
| 473 | @classmethod
|
||
| 474 | def generate_rrd_name_from(cls, keyTuple): |
||
| 475 | assert isinstance(keyTuple, TupleType) or isinstance(keyTuple, ListType) |
||
| 476 | # we have to check, whether all tuple-elements have values
|
||
| 477 | l = [] |
||
| 478 | for key in keyTuple: |
||
| 479 | key = key if key else "local" |
||
| 480 | l.append(key) |
||
| 481 | return string.join(l).lower().translate(cls.translationTable)
|
||
| 482 | |||
| 483 | def addPackage(self, sourceFile, package): |
||
| 484 | if self.packetHandler.decider(package): |
||
| 485 | self.packetHandler.adder(package, self) |
||
| 486 | |||
| 487 | @classmethod
|
||
| 488 | def configD(cls, key, value): |
||
| 489 | i = { 'rrdName': cls.generate_rrd_name_from(key),
|
||
| 490 | 'options': string.join(key,'/'), |
||
| 491 | 'info' : "from %r" % value.asLine() } |
||
| 492 | return i
|
||
| 493 | |||
| 494 | def configHead(self): |
||
| 495 | d = { 'graphName': "packages_"+ self.generate_rrd_name_from(self.option),
|
||
| 496 | 'option': string.join(self.option, '/'), |
||
| 497 | 'type' : self.packetHandler.type |
||
| 498 | } |
||
| 499 | return "\n"\ |
||
| 500 | "multigraph {graphName}_{type}\n"\
|
||
| 501 | "graph_title {type} Debian packages sorted by {option}\n"\
|
||
| 502 | "graph_info {type} Debian packages sorted by {option} of its repository\n"\
|
||
| 503 | d3ec2928 | dipohl | "graph_category security\n"\
|
| 504 | b64fa218 | Björn Lässig | "graph_vlabel packages".format(**d)
|
| 505 | |||
| 506 | def printConfig(self): |
||
| 507 | print self.configHead() |
||
| 508 | for options, item in self.options_sorted: |
||
| 509 | if not self.packetHandler.includeNow and self.optionIsDpkgStatus(details=options): |
||
| 510 | continue
|
||
| 511 | i = self.configD(options, item)
|
||
| 512 | print "{rrdName}.label {options}".format(**i) |
||
| 513 | print "{rrdName}.info {info}".format(**i) |
||
| 514 | print "{rrdName}.draw AREASTACK".format(**i) |
||
| 515 | |||
| 516 | def optionIsDpkgStatus(self, details, options=None): |
||
| 517 | """
|
||
| 518 | give it details and options and it tells you whether the datails looks like they come from
|
||
| 519 | a 'Debian dpkg status file'.
|
||
| 520 | """
|
||
| 521 | # setting defaults
|
||
| 522 | if options is None: |
||
| 523 | options = self.option
|
||
| 524 | assert type(details) in (TupleType, ListType), 'details must be tuple or list not %r' % type(details) |
||
| 525 | assert type(options) in (TupleType, ListType), 'options must be tuple or list not %r' % type(details) |
||
| 526 | assert len(details) == len(options) |
||
| 527 | isNow = True
|
||
| 528 | for det, opt in zip(details, options): |
||
| 529 | isNow &= self.dpkgStatusValue[opt] == det
|
||
| 530 | return isNow
|
||
| 531 | |||
| 532 | def printValues(self): |
||
| 533 | print "\nmultigraph packages_{option}_{type}".format(option=self.generate_rrd_name_from(self.option), |
||
| 534 | type=self.packetHandler.type)
|
||
| 535 | for options, item in self.options_sorted: |
||
| 536 | if not self.packetHandler.includeNow and self.optionIsDpkgStatus(details=options): |
||
| 537 | continue
|
||
| 538 | i = self.configD(options, item)
|
||
| 539 | i['value'] = len(self.get(options, [])) |
||
| 540 | print "{rrdName}.value {value}".format(**i) |
||
| 541 | self._printExtInfoPackageList(options)
|
||
| 542 | |||
| 543 | def _printExtInfoPackageList(self, options): |
||
| 544 | rrdName = self.generate_rrd_name_from(options)
|
||
| 545 | packageList = self[options]
|
||
| 546 | packageCount = len( packageList )
|
||
| 547 | if 0 < packageCount <= MAX_LIST_SIZE_EXT_INFO: |
||
| 548 | print "%s.extinfo " % rrdName, |
||
| 549 | for item in packageList: |
||
| 550 | print self.packetHandler.extInfoItemString.format(i=item), |
||
| 551 | print
|
||
| 552 | |||
| 553 | packetHandlerD = {}
|
||
| 554 | """ Dictionary for PacketHandlerclasses with its 'type'-key """
|
||
| 555 | |||
| 556 | class PacketHandler(object): |
||
| 557 | """
|
||
| 558 | Baseclass, that represents the Interface which is used
|
||
| 559 | """
|
||
| 560 | |||
| 561 | type = None
|
||
| 562 | includeNow = None
|
||
| 563 | extInfoItemString = None
|
||
| 564 | |||
| 565 | def __init__(self, apt): |
||
| 566 | self.apt = apt
|
||
| 567 | |||
| 568 | def decider(self, package, *args, **kwords): |
||
| 569 | """
|
||
| 570 | Function works as decider
|
||
| 571 | if it returns True, the package is added
|
||
| 572 | if it returns False, the package is not added
|
||
| 573 | """
|
||
| 574 | pass
|
||
| 575 | |||
| 576 | def adder(self, package, packageStat, *args, **kwords): |
||
| 577 | """
|
||
| 578 | take the package and add it tho the packageStat dictionary in defined way
|
||
| 579 | """
|
||
| 580 | pass
|
||
| 581 | |||
| 582 | @classmethod
|
||
| 583 | def keyOf(cls, pFile): |
||
| 584 | """
|
||
| 585 | calculates the weight of a apt_pkg.PackageFile
|
||
| 586 | """
|
||
| 587 | options = ('origin', 'site', 'archive', 'component', 'label') |
||
| 588 | details = tuple()
|
||
| 589 | for option in options: |
||
| 590 | details = details.__add__((pFile.__getattribute__(option),)) |
||
| 591 | return weightOfPackageFile(details, options)
|
||
| 592 | |||
| 593 | class PacketHandlerUpgradable(PacketHandler): |
||
| 594 | |||
| 595 | type='upgradable'
|
||
| 596 | includeNow = False
|
||
| 597 | extInfoItemString = " {i[0].name} <{i[1]} -> {i[2]}>"
|
||
| 598 | |||
| 599 | def decider(self, package, *args, **kwords): |
||
| 600 | return self.apt.depcache.is_upgradable(package) |
||
| 601 | |||
| 602 | def adder(self, package, packageStat, *args, **kwords): |
||
| 603 | options = tuple(packageStat.option)
|
||
| 604 | candidateP = self.apt.depcache.get_candidate_ver(package)
|
||
| 605 | candidateFile = max(candidateP.file_list, key=lambda f: self.keyOf(f[0]) )[0] |
||
| 606 | keys = createKey(options, candidateFile) |
||
| 607 | # this item (as i) is used for input in extInfoItemString
|
||
| 608 | item = (package, package.current_ver.ver_str, candidateP.ver_str) |
||
| 609 | packageStat[keys].append(item) |
||
| 610 | |||
| 611 | # registering PackageHandler for Usage
|
||
| 612 | packetHandlerD[PacketHandlerUpgradable.type] = PacketHandlerUpgradable |
||
| 613 | |||
| 614 | class PacketHandlerInstalled(PacketHandler): |
||
| 615 | type = 'installed'
|
||
| 616 | includeNow = True
|
||
| 617 | extInfoItemString = " {i.name}"
|
||
| 618 | |||
| 619 | def decider(self, package, *args, **kwords): |
||
| 620 | # this function is called with each installed package
|
||
| 621 | return True |
||
| 622 | |||
| 623 | def adder(self, package, packageStat, *args, **kwords): |
||
| 624 | options = tuple(packageStat.option)
|
||
| 625 | candidateP = self.apt.depcache.get_candidate_ver(package)
|
||
| 626 | candidateFile = max(candidateP.file_list, key=lambda f: self.keyOf(f[0]) )[0] |
||
| 627 | keys = createKey(options, candidateFile) |
||
| 628 | # this item (as i) is used for input in extInfoItemString
|
||
| 629 | item = package |
||
| 630 | packageStat[keys].append(item) |
||
| 631 | |||
| 632 | # registering PackageHandler for Usage
|
||
| 633 | packetHandlerD[PacketHandlerInstalled.type] = PacketHandlerInstalled |
||
| 634 | |||
| 635 | class Munin(object): |
||
| 636 | |||
| 637 | def __init__(self, commandLineArgs=None): |
||
| 638 | self.commandLineArgs = commandLineArgs
|
||
| 639 | self.argParser = self._argParser() |
||
| 640 | self.executionMatrix = {
|
||
| 641 | 'config': self.config, |
||
| 642 | 'run' : self.run, |
||
| 643 | 'autoconf' : self.autoconf, |
||
| 644 | } |
||
| 645 | self.envConfig = self._envParser() |
||
| 646 | self._envValidater()
|
||
| 647 | # print >> sys.stderr, self.envConfig
|
||
| 648 | self.statL = []
|
||
| 649 | if self.envConfig: |
||
| 650 | for config in self.envConfig: |
||
| 651 | packetHandler = packetHandlerD[config['type']](apt)
|
||
| 652 | packageStat = PackageStat(apt=apt, |
||
| 653 | packetHandler = packetHandler, |
||
| 654 | sortBy = config['sort_by'],
|
||
| 655 | extInfo = config['show_ext'])
|
||
| 656 | self.statL.append(packageStat)
|
||
| 657 | if not self.statL: |
||
| 658 | print "# no munin config found in environment vars" |
||
| 659 | |||
| 660 | def execute(self): |
||
| 661 | self.args = self.argParser.parse_args(self.commandLineArgs) |
||
| 662 | self.executionMatrix[self.args.command]() |
||
| 663 | |||
| 664 | def _cacheIsOutdated(self): |
||
| 665 | """
|
||
| 666 | # interesting files are pkgcache.bin (if it exists (it is deleted after apt-get clean))
|
||
| 667 | # if a file is intstalled or upgraded, '/var/lib/dpkg/status' is changed
|
||
| 668 | """
|
||
| 669 | if os.path.isfile(CACHE_FILE):
|
||
| 670 | cacheMTime = os.stat(CACHE_FILE).st_mtime |
||
| 671 | else:
|
||
| 672 | # no cachestatus file exist, so it _must_ renewed
|
||
| 673 | return True |
||
| 674 | # List of modify-times of different files
|
||
| 675 | timeL = [] |
||
| 676 | packageListsDir = "/var/lib/apt/lists"
|
||
| 677 | files=os.listdir(packageListsDir) |
||
| 678 | packageFileL = [ file for file in files if file.endswith('Packages')] |
||
| 679 | for packageFile in packageFileL: |
||
| 680 | timeL.append(os.stat(os.path.join(packageListsDir, packageFile)).st_mtime) |
||
| 681 | |||
| 682 | dpkgStatusFile = '/var/lib/dpkg/status'
|
||
| 683 | if os.path.isfile(dpkgStatusFile):
|
||
| 684 | timeL.append(os.stat(dpkgStatusFile).st_mtime) |
||
| 685 | else:
|
||
| 686 | raise Exception('DPKG-statusfile %r not found, really strange!!!'%dpkgStatusFile) |
||
| 687 | newestFileTimestamp = max(timeL)
|
||
| 688 | age = newestFileTimestamp - cacheMTime |
||
| 689 | if age > 0: |
||
| 690 | return True |
||
| 691 | else:
|
||
| 692 | # if we have made a timetravel, we update until we reached good times
|
||
| 693 | if time() < newestFileTimestamp:
|
||
| 694 | return True |
||
| 695 | return False |
||
| 696 | |||
| 697 | def _run_with_cache(self): |
||
| 698 | """ wrapper around _run with writing to file and stdout
|
||
| 699 | a better way would be a 'shell' tee as stdout
|
||
| 700 | """
|
||
| 701 | # cacheNeedUpdate = False
|
||
| 702 | # if not self.args.nocache:
|
||
| 703 | # # check, whether the cachefile has to be written again
|
||
| 704 | # if os.path.isfile(CACHE_FILE):
|
||
| 705 | # mtime = os.stat(CACHE_FILE).st_mtime
|
||
| 706 | # age = time() - mtime
|
||
| 707 | # cacheNeedUpdate = age < 0 or age > CACHE_FILE_MAX_AGE
|
||
| 708 | # else:
|
||
| 709 | # cacheNeedUpdate = True
|
||
| 710 | |||
| 711 | if self._cacheIsOutdated() or self.args.nocache: |
||
| 712 | # save stdout
|
||
| 713 | stdoutDef = sys.stdout |
||
| 714 | try:
|
||
| 715 | out = StringIO.StringIO() |
||
| 716 | sys.stdout = out |
||
| 717 | # run writes now to new sys.stdout
|
||
| 718 | print "# executed at %r (%r)" %(strftime("%s"), strftime("%c")) |
||
| 719 | self._run()
|
||
| 720 | sys.stdout = stdoutDef |
||
| 721 | # print output to stdout
|
||
| 722 | stdoutDef.write(out.getvalue()) |
||
| 723 | # print output to CACHE_FILE
|
||
| 724 | with open(CACHE_FILE,'w') as state: |
||
| 725 | state.write(out.getvalue()) |
||
| 726 | except IOError as e: |
||
| 727 | if e.errno == 2: |
||
| 728 | sys.stderr.write("%s : %s" % (e.msg, CACHE_FILE))
|
||
| 729 | # 'No such file or directory'
|
||
| 730 | os.makedirs( os.path.dirname(CACHE_FILE) ) |
||
| 731 | else:
|
||
| 732 | print sys.stderr.write("%r : %r" % (e, CACHE_FILE)) |
||
| 733 | finally:
|
||
| 734 | # restore stdout
|
||
| 735 | sys.stdout = stdoutDef |
||
| 736 | else:
|
||
| 737 | with open(CACHE_FILE,'r') as data: |
||
| 738 | print data.read()
|
||
| 739 | |||
| 740 | def _run(self): |
||
| 741 | # p … package
|
||
| 742 | # do the real work
|
||
| 743 | for p in apt.installedPackages: |
||
| 744 | sourceFile = max(p.current_ver.file_list, key=lambda f: PacketHandler.keyOf(f[0]) )[0] |
||
| 745 | for packageStat in self.statL: |
||
| 746 | packageStat.addPackage(sourceFile, p) |
||
| 747 | |||
| 748 | # print munin output
|
||
| 749 | for stat in self.statL: |
||
| 750 | stat.printValues() |
||
| 751 | |||
| 752 | def run(self): |
||
| 753 | if self.args.nocache: |
||
| 754 | self._run()
|
||
| 755 | else:
|
||
| 756 | self._run_with_cache()
|
||
| 757 | |||
| 758 | def config(self): |
||
| 759 | for stat in self.statL: |
||
| 760 | stat.printConfig() |
||
| 761 | |||
| 762 | def autoconf(self): |
||
| 763 | print 'yes' |
||
| 764 | |||
| 765 | def _argParser(self): |
||
| 766 | parser = argparse.ArgumentParser(description="Show some statistics "\
|
||
| 767 | "about debian packages installed on system by archive",
|
||
| 768 | ) |
||
| 769 | parser.set_defaults(command='run', debug=True, nocache=True) |
||
| 770 | |||
| 771 | parser.add_argument('--nocache', '-n', default=False, action='store_true', |
||
| 772 | help='do not use a cache file')
|
||
| 773 | helpCommand = """
|
||
| 774 | config ..... writes munin config
|
||
| 775 | run ........ munin run (writes values)
|
||
| 776 | autoconf ... writes 'yes'
|
||
| 777 | """
|
||
| 778 | parser.add_argument('command', nargs='?', |
||
| 779 | choices=['config', 'run', 'autoconf', 'drun'], |
||
| 780 | help='mode munin wants to use. "run" is default' + helpCommand)
|
||
| 781 | return parser
|
||
| 782 | |||
| 783 | def _envParser(self): |
||
| 784 | """
|
||
| 785 | reads environVars from [deb_packages] and generate
|
||
| 786 | a list of dicts, each dict holds a set of settings made in
|
||
| 787 | munin config.
|
||
| 788 | [
|
||
| 789 | { 'type' = 'installed',
|
||
| 790 | 'sort_by' = ['label', 'archive'],
|
||
| 791 | 'show_ext' = ['origin', 'site'],
|
||
| 792 | },
|
||
| 793 | { 'type' = 'upgraded',
|
||
| 794 | 'sort_by' = ['label', 'archive'],
|
||
| 795 | 'show_ext' = ['origin', 'site'],
|
||
| 796 | }
|
||
| 797 | ]
|
||
| 798 | """
|
||
| 799 | def configStartDict(): |
||
| 800 | return { 'type': None, |
||
| 801 | 'sort_by': dict(), |
||
| 802 | 'show_ext' : dict(), |
||
| 803 | } |
||
| 804 | |||
| 805 | interestingVarNameL = [ var for var in os.environ if var.startswith('graph') ] |
||
| 806 | config = defaultdict(configStartDict) |
||
| 807 | regex = re.compile(r"graph(?P<graphNumber>\d+)_(?P<res>.*?)_?(?P<optNumber>\d+)?$")
|
||
| 808 | for var in interestingVarNameL: |
||
| 809 | m = re.match(regex, var) |
||
| 810 | configPart = config[m.group('graphNumber')]
|
||
| 811 | if m.group('res') == 'type': |
||
| 812 | configPart['type'] = os.getenv(var)
|
||
| 813 | elif m.group('res') == 'sort_by': |
||
| 814 | configPart['sort_by'][m.group('optNumber')] = os.getenv(var) |
||
| 815 | elif m.group('res') == 'show_ext': |
||
| 816 | configPart['show_ext'][m.group('optNumber')] = os.getenv(var) |
||
| 817 | else:
|
||
| 818 | print >> sys.stderr, "configuration option %r was ignored" % (var) |
||
| 819 | # we have now dicts for 'sort_by' and 'show_ext' keys
|
||
| 820 | # changing them to lists
|
||
| 821 | for graphConfig in config.itervalues(): |
||
| 822 | graphConfig['sort_by'] = [val for key, val in sorted(graphConfig['sort_by'].items())] |
||
| 823 | graphConfig['show_ext'] = [val for key, val in sorted(graphConfig['show_ext'].items())] |
||
| 824 | # we do not want keynames, they are only needed for sorting environmentvars
|
||
| 825 | return [val for key, val in sorted(config.items())] |
||
| 826 | |||
| 827 | def _envValidater(self): |
||
| 828 | """ takes the munin config and checks for valid configuration,
|
||
| 829 | raises Exception if something is broken
|
||
| 830 | """
|
||
| 831 | for graph in self.envConfig: |
||
| 832 | if graph['type'] not in ('installed', 'upgradable'): |
||
| 833 | print >> sys.stderr, \
|
||
| 834 | "GraphType must be 'installed' or 'upgradable' but not %r"%(graph.type), \
|
||
| 835 | graph |
||
| 836 | raise EnvironmentConfigBroken("Environment Config broken") |
||
| 837 | if not graph['sort_by']: |
||
| 838 | print >> sys.stderr, \
|
||
| 839 | "Graph must be sorted by anything"
|
||
| 840 | raise EnvironmentConfigBroken("Environment Config broken") |
||
| 841 | # check for valid options for sort_by
|
||
| 842 | unusableOptions = set(graph['sort_by']) - PackageStat.viewSet |
||
| 843 | if unusableOptions:
|
||
| 844 | print >> sys.stderr, \
|
||
| 845 | "%r are not valid options for 'sort_by'" % (unusableOptions)
|
||
| 846 | raise EnvironmentConfigBroken("Environment Config broken") |
||
| 847 | # check for valid options for sort_by
|
||
| 848 | unusableOptions = set(graph['show_ext']) - PackageStat.viewSet |
||
| 849 | if unusableOptions:
|
||
| 850 | print >> sys.stderr, \
|
||
| 851 | "%r are not valid options for 'show_ext'" % (x)
|
||
| 852 | raise EnvironmentConfigBroken("Environment Config broken") |
||
| 853 | |||
| 854 | if __name__=='__main__': |
||
| 855 | muninPlugin = Munin() |
||
| 856 | muninPlugin.execute() |
||
| 857 | # import IPython; IPython.embed()
|
||
| 858 | d3ec2928 | dipohl | |
| 859 | |||
| 860 | ### The following is the smart_ plugin documentation, intended to be used with munindoc
|
||
| 861 | |||
| 862 | """
|
||
| 863 | =head1 NAME
|
||
| 864 |
|
||
| 865 | deb_packages - plugin to monitor update resources and pending packages on Debian
|
||
| 866 |
|
||
| 867 | =head1 APPLICABLE SYSTEMS
|
||
| 868 |
|
||
| 869 | This plugin has checked on Debian - Wheezy and squeeze. If you want to use it
|
||
| 870 | on older installations, tell me whether it works or which errors you had. It
|
||
| 871 | shoud run past python-apt 0.7 and python 2.5.
|
||
| 872 |
|
||
| 873 | =head1 DESCRIPTION
|
||
| 874 |
|
||
| 875 | With this plugin munin can give you a nice graph and some details where your
|
||
| 876 | packages come from, how old or new your installation is. Furtermore it tells
|
||
| 877 | you how many updates you should have been installed, how many packages are
|
||
| 878 | outdated and where they come from.
|
||
| 879 |
|
||
| 880 | You can sort installed or upgradable Packages by 'archive', 'origin', 'site',
|
||
| 881 | 'label' and 'component' and even some of them at once.
|
||
| 882 |
|
||
| 883 | The script uses caching cause it is quite expensive. It saves the output to a
|
||
| 884 | cachefile and checks on each run, if dpkg-status or downloaded Packagefile have
|
||
| 885 | changed. If one of them has changed, it runs, if not it gives you the cached
|
||
| 886 | version
|
||
| 887 |
|
||
| 888 | =head1 INSTALLATION
|
||
| 889 |
|
||
| 890 | check out this git repository from
|
||
| 891 |
|
||
| 892 | =over 2
|
||
| 893 |
|
||
| 894 | aptitude install python-apt
|
||
| 895 | git clone git://github.com/munin-monitoring/contrib.git
|
||
| 896 | cd contrib/plugins/apt/deb_packages
|
||
| 897 | sudo cp deb_packages.py /etc/munin/plugins/deb_packages
|
||
| 898 | sudo cp deb_packages.munin-conf /etc/munin/plugin-conf.d/deb_packages
|
||
| 899 |
|
||
| 900 | =back
|
||
| 901 |
|
||
| 902 | Verify the installation by
|
||
| 903 |
|
||
| 904 | =over 2
|
||
| 905 |
|
||
| 906 | sudo munin-run deb_packages
|
||
| 907 |
|
||
| 908 | =back
|
||
| 909 |
|
||
| 910 |
|
||
| 911 | =head1 CONFIGURATION
|
||
| 912 |
|
||
| 913 | If you copied deb_packages.munin-conf to plugin-conf.d you have a starting point.
|
||
| 914 |
|
||
| 915 | A typical configuration looks like this
|
||
| 916 |
|
||
| 917 | =over 2
|
||
| 918 |
|
||
| 919 | [deb_packages]
|
||
| 920 | # plugin is quite expensive and has to write statistics to cache output
|
||
| 921 | # so it has to write to plugins.cache
|
||
| 922 | user munin
|
||
| 923 |
|
||
| 924 | # Packagelists to this size are printed as extra information to munin.extinfo
|
||
| 925 | env.MAX_LIST_SIZE_EXT_INFO 50
|
||
| 926 |
|
||
| 927 | # Age in seconds an $CACHE_FILE can be. If it is older, the script updates
|
||
| 928 | # default if not set is 3540 (one hour)
|
||
| 929 | # at the moment this is not used, the plugin always runs (if munin calls it)
|
||
| 930 | #
|
||
| 931 | env.CACHE_FILE_MAX_AGE 3540
|
||
| 932 |
|
||
| 933 | # All these numbers are only for sorting, so you can use env.graph01_sort_by_0
|
||
| 934 | # and env.graph01_sort_by_2 without using env.graph01_sort_by_1.
|
||
| 935 | # sort_by values ...
|
||
| 936 | # possible values are 'label', 'archive', 'origin', 'site', 'component'
|
||
| 937 | env.graph00_type installed
|
||
| 938 | env.graph00_sort_by_0 label
|
||
| 939 | env.graph00_sort_by_1 archive
|
||
| 940 | env.graph00_show_ext_0 origin
|
||
| 941 | env.graph00_show_ext_1 site
|
||
| 942 |
|
||
| 943 | env.graph01_type upgradable
|
||
| 944 | env.graph01_sort_by_0 label
|
||
| 945 | env.graph01_sort_by_1 archive
|
||
| 946 | env.graph01_show_ext_0 origin
|
||
| 947 | env.graph01_show_ext_1 site
|
||
| 948 |
|
||
| 949 | =back
|
||
| 950 |
|
||
| 951 | You can sort_by one or some of these possible Values
|
||
| 952 |
|
||
| 953 |
|
||
| 954 | =head1 AUTHOR
|
||
| 955 |
|
||
| 956 | unknown
|
||
| 957 |
|
||
| 958 | =head1 LICENSE
|
||
| 959 |
|
||
| 960 | Default for Munin contributions is GPLv2 (http://www.gnu.org/licenses/gpl-2.0.txt)
|
||
| 961 |
|
||
| 962 | =cut
|
||
| 963 |
|
||
| 964 |
|
||
| 965 | """ |
