Projet

Général

Profil

Paste
Télécharger au format
Statistiques
| Branche: | Révision:

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
"""