root / plugins / apt / deb_packages / deb_packages.py @ 8589c6df
Historique | Voir | Annoter | Télécharger (34 ko)
| 1 |
#!/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 |
BUG: If a package will be upgraded, and brings in new dependencies,
|
| 28 |
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 |
tells you which representation of a tree as line is shortest.
|
| 261 |
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 |
"graph_category security\n"\
|
| 504 |
"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 |
|
| 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 |
"""
|
