#!/usr/bin/python
# -*- coding: utf-8 -*-

""" 
A munin plugin that prints archive and their upgradable packets

TODO: make it usable and readable as commandline tool
 • (-i) interaktiv
   NICETOHAVE
TODO: separate into 2 graphs
 • how old is my deb installation
   sorting a packet to the oldest archive
   sorting a packet to the newest archive
   (WONTFIX unless someone asks for)

TODO: 
 • addinge alternative names for archives "stable -> squeeze"
TODO: add gray as 
      foo.colour 000000
      to 'now', '', '', '', '', 'Debian dpkg status file'
TODO: update only if system was updated (aptitutde update has been run)
      • check modification date of /var/cache/apt/pkgcache.bin
      • cache file must not be older than mod_date of pkgcache.bin + X
TODO: shorten ext_info with getShortestConfigOfOptions 
TODO: check whether cachefile matches the config 
      • i have no clever idea to do this without 100 lines of code
BUG: If a package will be upgraded, and brings in new dependancies, 
     these new deps will not be counted. WONTFIX
"""
import sys
import argparse
import apt_pkg
from apt.progress.base import OpProgress
from time import time, strftime
import os 
import StringIO
import string
import re
from collections  import defaultdict, namedtuple
from types import StringTypes, TupleType, DictType, ListType, BooleanType

class EnvironmentConfigBroken(Exception): pass

# print environmental things 
# for k,v in os.environ.iteritems(): print >> sys.stderr, "%r : %r" % (k,v)

def getEnv(name, default=None, cast=None):
    """
        function to get Environmentvars, cast them and setting defaults if they aren't
        getEnv('USER', default='nouser') # 'HomerS'
        getEnv('WINDOWID', cast=int) # 44040201
    """
    try:
        var = os.environ[name]
        if cast is not None:
            var = cast(var)
    except KeyError:
        # environment does not have this var
        var = default
    except:
        # now probably the cast went wrong
        print >> sys.stderr, "for environment variable %r, %r is no valid value"%(name, var)
        var = default
    return var

MAX_LIST_SIZE_EXT_INFO = getEnv('MAX_LIST_SIZE_EXT_INFO', default=50, cast=int)
""" Packagelists to this size are printed as extra Information to munin """

STATE_DIR = getEnv('MUNIN_PLUGSTATE', default='.')
CACHE_FILE = os.path.join(STATE_DIR, "deb_packages.state")
""" 
   There is no need to execute this script every 5 minutes.
   The Results are put to this file, next munin-run can read from it
   CACHE_FILE is usually /var/lib/munin/plugin-state/debian_packages.state
"""

CACHE_FILE_MAX_AGE = getEnv('CACHE_FILE_MAX_AGE', default=3540, cast=int)
""" 
   Age in seconds an $CACHE_FILE can be. If it is older, the script updates
"""

def Property(func):
    return property(**func())

class Apt(object):
    """
        lazy helperclass i need in this statisticprogram, which have alle the apt_pkg stuff
    """

    def __init__(self):
        # init packagesystem
        apt_pkg.init_config()
        apt_pkg.init_system()
        # NullProgress : we do not want progress info in munin plugin
        # documented None did not worked
        self._cache = None
        self._depcache = None
        self._installedPackages = None
        self._upgradablePackages = None

    @Property
    def cache():
        doc = "apt_pkg.Cache instance, lazy instantiated"
        def fget(self):
            class NullProgress(OpProgress):
                """ used for do not giving any progress info, 
                    while doing apt things used, cause documented 
                    use of None as OpProgress did not worked in 
                    python-apt 0.7
                """
                def __init__(self):
                    self.op=''
                    self.percent=0
                    self.subop=''

                def done(self):
                    pass

                def update(*args,**kwords):
                    pass

            if self._cache is None: 
                self._cache = apt_pkg.Cache(NullProgress()) 
            return self._cache
        return locals()

    @Property
    def depcache():
        doc = "apt_pkg.DepCache object"

        def fget(self):
            if self._depcache is None: 
                self._depcache = apt_pkg.DepCache(self.cache)
            return self._depcache

        return locals()

    @Property
    def installedPackages():
        doc = """apt_pkg.PackageList with installed Packages
                 it is a simple ListType with Elements of apt_pkg.Package
              """

        def fget(self):
            """ returns a apt_pkg.PackageList with installed Packages
                it is a simple ListType with Elements of apt_pkg.Package
            """
            if self._installedPackages is None:
                self._installedPackages = []
                for p in self.cache.packages:
                    if not ( p.current_state == apt_pkg.CURSTATE_NOT_INSTALLED or
                             p.current_state == apt_pkg.CURSTATE_CONFIG_FILES ):
                        self._installedPackages.append(p)
            return self._installedPackages

        return locals()

    @Property
    def upgradablePackages():

        doc = """apt_pkg.PackageList with Packages that are upgradable
                 it is a simple ListType with Elements of apt_pkg.Package
              """

        def fget(self):
            if self._upgradablePackages is None:
                self._upgradablePackages = []
                for p in self.installedPackages:
                    if self.depcache.is_upgradable(p):
                        self._upgradablePackages.append(p)
            return self._upgradablePackages

        return locals()

apt = Apt()
""" global instance of apt data, used here

        apt.cache
        apt.depcache
        apt.installedPackages
        apt.upgradablePackages

    initialisation is lazy 
"""

def weightOfPackageFile(detail_tuple, option_tuple):
    """
        calculates a weight, you can sort with
        if detail_tuple is: ['label', 'archive']
           option_tuple is: ['Debian', 'unstable']
        it calculates
            sortDict['label']['Debian'] * multiplierDict['label']
          + sortDict['archive']['unstable'] * multiplierDict['archive']
          = 10 * 10**4 + 50 * 10**8
          = 5000100000
    """
    val = 0L
    for option, detail in zip(option_tuple, detail_tuple):
        optionValue = PackageStat.sortDict[option][detail]
        val += optionValue * PackageStat.multiplierDict[option]
    return val

def Tree():
    """ Tree type generator
        you can put data at the end of a twig
        a = Tree()
        a['a']['b']['c'] # creates the tree of depth 3
        a['a']['b']['d'] # creates another twig of the tree
                c
        a — b <
                d
    """
    return TreeTwig(Tree)

class TreeTwig(defaultdict):
    def __init__(self, defaultFactory):
        super(TreeTwig, self).__init__(defaultFactory) 

    def printAsTree(self, indent=0):
        for k, tree in self.iteritems():
            print "  " * indent, repr(k)
            if isinstance(tree, TreeTwig):
                printTree(tree, indent+1)
            else:
                print tree

    def printAsLine(self):
        print self.asLine()

    def asLine(self):
        values = ""
        for key, residue in self.iteritems():
            if residue:
                values += " %r" % key
                if isinstance(residue, TreeTwig):
                    if len(residue) == 1:
                        values += " - %s" % residue.asLine()
                    else:
                        values += "(%s)" % residue.asLine()
                else:
                    values += "(%s)" % residue
            else:
                values += " %r," % key
        return values.strip(' ,')


def getShortestConfigOfOptions(optionList = ['label', 'archive', 'site']):
    """ 
        tries to find the order to print a tree of the optionList
        with the local repositories with the shortest line 
        possible options are:
          'component'
          'label'
          'site'
          'archive'
          'origin' 
          'architecture' 
        Architecture values are usually the same and can be ignored.

        tells you wich representation of a tree as line is shortest.
        Is needed to say which ext.info line would be the shortest
        to write the shortest readable output.
    """
    l = optionList # just because l is much shorter
    
    # creating possible iterations
    fieldCount = len(optionList)
    if fieldCount == 1:
        selection = l
    elif fieldCount == 2:
        selection = [(x,y) 
                     for x in l 
                     for y in l if x!=y ]
    elif fieldCount == 3:
        selection = [(x,y,z) 
                     for x in l 
                     for y in l if x!=y 
                     for z in l if z!=y and z!=x]
    else:
        raise Exception("NotImplemented for size %s" % fieldCount)

    # creating OptionsTree, and measuring the length of it on a line
    # for every iteration
    d = {}
    for keys in selection:
        d[keys] = len( getOptionsTree(apt.cache, keys).asLine() )

    # finding the shortest variant
    r = min( d.items(), key=lambda x: x[1] )

    return list(r[0]), r[1]
    
def getOptionsTree(cache, keys=None):
    """
    t = getOptionsTree(cache, ['archive', 'site', 'label'])
    generates ad dict of dict of sets like:
    ...
    it tells you:
    ...
    """
    t = Tree()
    for f in cache.file_list:
        # ignoring translation indexes ...
        if f.index_type != 'Debian Package Index' and f.index_type !='Debian dpkg status file':
            continue
        # ignoring files with 0 size
        if f.size == 0L:
            continue
        # creating default dict in case of secondary_options are empty
        d = t
        for key in keys:
            if not key:
                print f
            dKey = f.__getattribute__(key)
            d = d[dKey]
    return t

def createKey(key, file):
    """
    createKey( (archive, origin), apt.pkg_file)
    returns ('unstable', 'Debian')
    """
    if type(key) in StringTypes:
        return file.__getattribute__(key)
    elif type(key) in (TupleType, ListType): 
        nKey = tuple()
        for pKey in key:
            nKey = nKey.__add__((file.__getattribute__(pKey),))
        return nKey
    else:
        raise Exception("Not implemented for keytype %s" % type(key)) 

def getOptionsTree2(cache, primary=None, secondary=None):
    """ 
    primary muss ein iterable oder StringType sein
    secondary muss iterable oder StringType sein
    t1 = getOptionsTree2(apt.cache, 'origin', ['site', 'archive'])
    t2 = getOptionsTree2(apt.cache, ['origin', 'archive'], ['site', 'label'])
    """


    if type(secondary) in StringTypes:
        secondary = [secondary]
    if type(primary) in StringTypes:
        primary = [primary]

    t = Tree()
    for file in cache.file_list:
        # ignoring translation indexes ...
        if file.index_type not in ['Debian Package Index', 'Debian dpkg status file']:
            continue
        # ignoring files with 0 size
        if file.size == 0L:
            continue

        # key to first Dict in Tree is a tuple
        pKey = createKey(primary, file)
        d = t[pKey]
        if secondary is not None:
            # for no, sKey in enumerate(secondary):
            #     dKey = file.__getattribute__(sKey)
            #     if no < len(secondary)-1:
            #         d = d[dKey]
            # if isinstance(d[dKey], DictType):
            #     d[dKey] = []
            # d[dKey].append(file)

            for sKey in secondary:
                dKey = file.__getattribute__(sKey)
                d = d[dKey]
    return t
    
#def getAttributeSet(iterable, attribute):
#    return set(f.__getattribute__(attribute) for f in iterable)
#
#def getOrigins(cache):
#    return getAttributeSet(cache.file_list, 'origin') 
#
#def getArchives(cache):
#    return getAttributeSet(cache.file_list, 'archive') 
#
#def getComponents(cache):
#    return getAttributeSet(cache.file_list, 'component') 
#
#def getLabels(cache):
#    return getAttributeSet(cache.file_list, 'label') 
#
#def getSites(cache):
#    return getAttributeSet(cache.file_list, 'site') 
#

class PackageStat(defaultdict):
    """ defaultdict with Tuple Keys of (label,archive) containing lists of ArchiveFiles
        {('Debian Backports', 'squeeze-backports'): [...]
         ('The Opera web browser', 'oldstable'): [...]
         ('Debian', 'unstable'): [...]}
        with some abilities to print output munin likes
    """

    sortDict = { 'label': defaultdict(   lambda :  20, 
                                       {'Debian':  90, 
                                        ''      :  1,
                                        'Debian Security' : 90,
                                        'Debian Backports': 90}),
                 'archive': defaultdict( lambda :  5,
                                { 'now':                0, 
                                  'experimental':      10,
                                  'unstable':          50, 
                                  'sid':               50, 
                                  'testing':           70,
                                  'wheezy':            70,
                                  'squeeze-backports': 80,
                                  'stable-backports':  80,
                                  'proposed-updates':  84,
                                  'stable-updates':    85,
                                  'stable':            90,
                                  'squeeze':           90,
                                  'oldstable':         95,
                                  'lenny':             95, } ),
                 'site': defaultdict( lambda :  5, { }),
                 'origin': defaultdict( lambda :  5, { 'Debian' : 90, }),
                 'component': defaultdict( lambda :  5, {
                                  'non-free': 10,
                                  'contrib' : 50,
                                  'main'    : 90, }),
    }
    """
        Values to sort options (label, archive, origin ...)
        (0..99) is allowed. 
        (this is needed for other graphs to calc aggregated weights)
        higher is more older and more official or better 
    """

    dpkgStatusValue = { 'site': '', 'origin': '', 'label': '', 'component': '', 'archive': 'now' }
    """ a dict to recognize options that coming from 'Debian dpkg status file' """

    viewSet = set(['label', 'archive', 'origin', 'site', 'component'])

    multiplierDict = { 'label'     : 10**8,
                       'archive'   : 10**4,
                       'site'      : 10**0,
                       'origin'    : 10**6,
                       'component' : 10**2,
    }
    """
        Dict that stores multipliers 
        to compile a sorting value for each archivefile
    """

    def weight(self, detail_tuple):
        return weightOfPackageFile(detail_tuple=detail_tuple, option_tuple=tuple(self.option))

    def __init__(self, packetHandler, apt=apt, sortBy=None, extInfo=None, includeNow=True, *args, **kwargs):
        assert isinstance(packetHandler, PacketHandler)
        self.packetHandler = packetHandler
        self.apt = apt
        self.option = sortBy if sortBy is not None else ['label', 'archive']
        optionsMentionedInExtInfo = extInfo if extInfo is not None else list(self.viewSet - set(self.option))
        self.options = getOptionsTree2(apt.cache, self.option, optionsMentionedInExtInfo)
        self.options_sorted = self._sorted(self.options.items())
        super(PackageStat, self).__init__(lambda: [], *args, **kwargs)

    translationTable = string.maketrans(' -.', '___')
    """ chars that must not exist in a munin system name"""

    @classmethod
    def generate_rrd_name_from(cls, string):
         return string.translate(cls.translationTable)

    def _sorted(self, key_value_pairs):
         return sorted(key_value_pairs, key=lambda(x): self.weight(x[0]), reverse=True)

    @classmethod
    def generate_rrd_name_from(cls, keyTuple):
        assert isinstance(keyTuple, TupleType) or isinstance(keyTuple, ListType)
        # we have to check, whether all tuple-elements have values
        l = []
        for key in keyTuple:
            key = key if key else "local"
            l.append(key)
        return string.join(l).lower().translate(cls.translationTable)

    def addPackage(self, sourceFile, package):
        if self.packetHandler.decider(package):
            self.packetHandler.adder(package, self)
            
    @classmethod
    def configD(cls, key, value):
        i = { 'rrdName': cls.generate_rrd_name_from(key),
              'options': string.join(key,'/'),
              'info'   : "from %r" % value.asLine() }
        return i

    def configHead(self):
        d = { 'graphName': "packages_"+ self.generate_rrd_name_from(self.option),
              'option': string.join(self.option, '/'),
              'type' : self.packetHandler.type
            }
        return "\n"\
               "multigraph {graphName}_{type}\n"\
               "graph_title {type} Debian packages sorted by {option}\n"\
               "graph_info {type} Debian packages sorted by {option} of its repository\n"\
               "graph_category debian\n"\
               "graph_vlabel packages".format(**d)

    def printConfig(self):
        print self.configHead()
        for options, item in self.options_sorted:
            if not self.packetHandler.includeNow and self.optionIsDpkgStatus(details=options):
                 continue
            i = self.configD(options, item)
            print "{rrdName}.label {options}".format(**i)
            print "{rrdName}.info {info}".format(**i)
            print "{rrdName}.draw AREASTACK".format(**i)

    def optionIsDpkgStatus(self, details, options=None):
        """ 
            give it details and options and it tells you whether the datails looks like they come from 
            a 'Debian dpkg status file'.
        """
        # setting defaults
        if options is None:
            options = self.option
        assert type(details) in (TupleType, ListType), 'details must be tuple or list not %r' % type(details)
        assert type(options) in (TupleType, ListType), 'options must be tuple or list not %r' % type(details)
        assert len(details) == len(options)
        isNow = True
        for det, opt in zip(details, options):
            isNow &= self.dpkgStatusValue[opt] == det
        return isNow

    def printValues(self):
        print "\nmultigraph packages_{option}_{type}".format(option=self.generate_rrd_name_from(self.option), 
                                                             type=self.packetHandler.type)
        for options, item in self.options_sorted:
            if not self.packetHandler.includeNow and self.optionIsDpkgStatus(details=options):
                 continue
            i = self.configD(options, item)
            i['value'] = len(self.get(options, []))
            print "{rrdName}.value {value}".format(**i)
            self._printExtInfoPackageList(options)

    def _printExtInfoPackageList(self, options):
        rrdName = self.generate_rrd_name_from(options)
        packageList = self[options]
        packageCount = len( packageList )
        if 0 < packageCount <= MAX_LIST_SIZE_EXT_INFO:
            print "%s.extinfo " % rrdName,
            for item in packageList:
                print self.packetHandler.extInfoItemString.format(i=item),
            print

packetHandlerD = {}
""" Dictionary for PacketHandlerclasses with its 'type'-key """

class PacketHandler(object):
    """
    Baseclass, that represents the Interface which is used 
    """

    type = None
    includeNow = None
    extInfoItemString = None

    def __init__(self, apt):
        self.apt = apt

    def decider(self, package, *args, **kwords):
        """
        Function works as decider
        if it returns True, the package is added
        if it returns False, the package is not added
        """
        pass

    def adder(self, package, packageStat, *args, **kwords):
        """
        take the package and add it tho the packageStat dictionary in defined way
        """
        pass

    @classmethod
    def keyOf(cls, pFile):
        """
        calculates the weight of a apt_pkg.PackageFile
        """
        options = ('origin', 'site', 'archive', 'component', 'label')
        details = tuple()
        for option in options:
            details = details.__add__((pFile.__getattribute__(option),))
        return weightOfPackageFile(details, options)

class PacketHandlerUpgradable(PacketHandler):
    
    type='upgradable'
    includeNow = False
    extInfoItemString = "  {i[0].name} <{i[1]} -> {i[2]}>"

    def decider(self, package, *args, **kwords):
        return self.apt.depcache.is_upgradable(package)

    def adder(self, package, packageStat, *args, **kwords):
        options = tuple(packageStat.option)
        candidateP = self.apt.depcache.get_candidate_ver(package)
        candidateFile = max(candidateP.file_list, key=lambda f: self.keyOf(f[0]) )[0]
        keys = createKey(options, candidateFile)
        # this item (as i) is used for input in extInfoItemString
        item = (package, package.current_ver.ver_str, candidateP.ver_str)
        packageStat[keys].append(item)

# registering PackageHandler for Usage
packetHandlerD[PacketHandlerUpgradable.type] = PacketHandlerUpgradable

class PacketHandlerInstalled(PacketHandler):
    type = 'installed'
    includeNow = True
    extInfoItemString = " {i.name}"

    def decider(self, package, *args, **kwords):
        # this function is called with each installed package
        return True

    def adder(self, package, packageStat, *args, **kwords):
        options = tuple(packageStat.option)
        candidateP = self.apt.depcache.get_candidate_ver(package)
        candidateFile = max(candidateP.file_list, key=lambda f: self.keyOf(f[0]) )[0]
        keys = createKey(options, candidateFile)
        # this item (as i) is used for input in extInfoItemString
        item = package
        packageStat[keys].append(item)
        
# registering PackageHandler for Usage
packetHandlerD[PacketHandlerInstalled.type] = PacketHandlerInstalled

class Munin(object):

    def __init__(self, commandLineArgs=None):
        self.commandLineArgs = commandLineArgs
        self.argParser = self._argParser()
        self.executionMatrix = { 
            'config': self.config,
            'run'   : self.run,
            'autoconf' : self.autoconf,
        }
        self.envConfig = self._envParser()
        self._envValidater()
        # print >> sys.stderr, self.envConfig
        self.statL = []
        if self.envConfig:
            for config in self.envConfig:
                packetHandler = packetHandlerD[config['type']](apt)
                packageStat = PackageStat(apt=apt,
                                                    packetHandler = packetHandler,
                                                    sortBy = config['sort_by'],
                                                    extInfo = config['show_ext'])
                self.statL.append(packageStat)
        if not self.statL:
            print "# no munin config found in environment vars"

    def execute(self):
        self.args = self.argParser.parse_args(self.commandLineArgs)
        self.executionMatrix[self.args.command]()

    def _cacheIsOutdated(self):
        """
        # interesting files are pkgcache.bin (if it exists (it is deleted after apt-get clean))
        # if a file is intstalled or upgraded, '/var/lib/dpkg/status' is changed
        """
        if os.path.isfile(CACHE_FILE):
            cacheMTime = os.stat(CACHE_FILE).st_mtime
        else:
            # no cachestatus file exist, so it _must_ renewed
            return True
        # List of modify-times of different files
        timeL = []
        packageListsDir = "/var/lib/apt/lists"
        files=os.listdir(packageListsDir)
        packageFileL = [ file for file in files if file.endswith('Packages')]
        for packageFile in packageFileL:
            timeL.append(os.stat(os.path.join(packageListsDir, packageFile)).st_mtime)

        dpkgStatusFile = '/var/lib/dpkg/status'
        if os.path.isfile(dpkgStatusFile):
            timeL.append(os.stat(dpkgStatusFile).st_mtime)
        else:
            raise Exception('DPKG-statusfile %r not found, really strange!!!'%dpkgStatusFile)
        newestFileTimestamp = max(timeL)
        age = newestFileTimestamp - cacheMTime 
        if age > 0:
            return True
        else:
            # if we have made a timetravel, we update until we reached good times
            if time() < newestFileTimestamp:
                return True
            return False

    def _run_with_cache(self):
        """ wrapper around _run with writing to file and stdout
            a better way would be a 'shell' tee as stdout
        """
        # cacheNeedUpdate = False
        # if not self.args.nocache:
        #     # check, whether the cachefile has to be written again
        #     if os.path.isfile(CACHE_FILE):
        #         mtime = os.stat(CACHE_FILE).st_mtime
        #         age = time() - mtime
        #         cacheNeedUpdate = age < 0 or age > CACHE_FILE_MAX_AGE
        #     else:
        #         cacheNeedUpdate = True

        if self._cacheIsOutdated() or self.args.nocache:
            # save stdout 
            stdoutDef = sys.stdout
            try:
                out =  StringIO.StringIO()
                sys.stdout = out
                # run writes now to new sys.stdout
                print "# executed at %r (%r)" %(strftime("%s"), strftime("%c"))
                self._run()
                sys.stdout = stdoutDef
                # print output to stdout
                stdoutDef.write(out.getvalue())
                # print output to CACHE_FILE
                with open(CACHE_FILE,'w') as state:
                    state.write(out.getvalue())
            except IOError as e:
                if e.errno == 2:
                    sys.stderr.write("%s : %s" % (e.msg, CACHE_FILE))
                    # 'No such file or directory'
                    os.makedirs( os.path.dirname(CACHE_FILE) )
                else:
                    print sys.stderr.write("%r : %r" % (e, CACHE_FILE))
            finally:
                # restore stdout
                sys.stdout = stdoutDef
        else:
            with open(CACHE_FILE,'r') as data:
                print data.read()

    def _run(self):
        # p … package
        # do the real work
        for p in apt.installedPackages:
            sourceFile = max(p.current_ver.file_list, key=lambda f: PacketHandler.keyOf(f[0]) )[0]
            for packageStat in self.statL:
                packageStat.addPackage(sourceFile, p)

        # print munin output
        for stat in self.statL:
            stat.printValues()

    def run(self):
        if self.args.nocache:
            self._run()
        else:
            self._run_with_cache()

    def config(self):
        for stat in self.statL:
            stat.printConfig()

    def autoconf(self):
        print 'yes'

    def _argParser(self):
        parser = argparse.ArgumentParser(description="Show some statistics "\
                            "about debian packages installed on system by archive",
                           ) 
        parser.set_defaults(command='run', debug=True, nocache=True)

        parser.add_argument('--nocache', '-n', default=False, action='store_true',
                            help='do not use a cache file')
        helpCommand = """
            config ..... writes munin config
            run ........ munin run (writes values)
            autoconf ... writes 'yes'
        """
        parser.add_argument('command', nargs='?', 
                            choices=['config', 'run', 'autoconf', 'drun'],
                            help='mode munin wants to use. "run" is default' + helpCommand)
        return parser

    def _envParser(self):
        """
            reads environVars from [deb_packages] and generate
            a list of dicts, each dict holds a set of settings made in 
            munin config.
            [ 
              { 'type' = 'installed', 
                'sort_by' = ['label', 'archive'],
                'show_ext' = ['origin', 'site'],
              },
              { 'type' = 'upgraded',
                'sort_by' = ['label', 'archive'],
                'show_ext' = ['origin', 'site'],
              }
            ]
        """
        def configStartDict():
            return { 'type': None,
                     'sort_by': dict(),
                     'show_ext' : dict(),
                   }

        interestingVarNameL = [ var for var in os.environ if var.startswith('graph') ]
        config = defaultdict(configStartDict)
        regex = re.compile(r"graph(?P<graphNumber>\d+)_(?P<res>.*?)_?(?P<optNumber>\d+)?$")
        for var in interestingVarNameL:
            m = re.match(regex, var)
            configPart = config[m.group('graphNumber')]
            if m.group('res') == 'type':
                configPart['type'] = os.getenv(var)
            elif m.group('res') == 'sort_by':
                configPart['sort_by'][m.group('optNumber')] = os.getenv(var)
            elif m.group('res') == 'show_ext':
                configPart['show_ext'][m.group('optNumber')] = os.getenv(var)
            else:
                print >> sys.stderr, "configuration option %r was ignored" % (var)
        # we have now dicts for 'sort_by' and 'show_ext' keys 
        # changing them to lists
        for graphConfig in config.itervalues():
            graphConfig['sort_by'] = [val for key, val in sorted(graphConfig['sort_by'].items())]
            graphConfig['show_ext'] = [val for key, val in sorted(graphConfig['show_ext'].items())]
        # we do not want keynames, they are only needed for sorting environmentvars
        return [val for key, val in sorted(config.items())]

    def _envValidater(self):
        """ takes the munin config and checks for valid configuration,
            raises Exception if something is broken
        """
        for graph in self.envConfig:
            if graph['type'] not in ('installed', 'upgradable'):
                print >> sys.stderr, \
                      "GraphType must be 'installed' or 'upgradable' but not %r"%(graph.type), \
                      graph
                raise EnvironmentConfigBroken("Environment Config broken")
            if not graph['sort_by']:
                print >> sys.stderr, \
                      "Graph must be sorted by anything"
                raise EnvironmentConfigBroken("Environment Config broken")
            # check for valid options for sort_by
            unusableOptions = set(graph['sort_by']) - PackageStat.viewSet 
            if unusableOptions: 
                print >> sys.stderr, \
                      "%r are not valid options for 'sort_by'" % (unusableOptions)
                raise EnvironmentConfigBroken("Environment Config broken")
            # check for valid options for sort_by
            unusableOptions = set(graph['show_ext']) - PackageStat.viewSet 
            if unusableOptions:
                print >> sys.stderr, \
                      "%r are not valid options for 'show_ext'" % (x)
                raise EnvironmentConfigBroken("Environment Config broken")

if __name__=='__main__':
    muninPlugin = Munin()
    muninPlugin.execute()
    # import IPython; IPython.embed()
