Skip to content
weboob-generic 14.9 KiB
Newer Older
Florent's avatar
Florent committed
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vim: ft=python et softtabstop=4 cinoptions=4 shiftwidth=4 ts=4 ai

# Copyright(C) 2013  Romain Bignon, Florent Fourcot
#
# This program is free software: you can redistribute it and/or modify
Florent's avatar
Florent committed
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
Florent's avatar
Florent committed
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
Florent's avatar
Florent committed

### Installation ###
# 1) Create a symlink from /etc/munin/plugins/yourchoice to the script
# 2) Configure the plugin in /etc/munin/plugin-conf.d/ See below for the options
# 3) Restart/reload munin-node
# 4) Note that cached values are stored in folder ~/.config/weboob/munin/

### Configuration ###
## Mandatory options ##
# env.capa: The Weboob capability to load
# Example: env.capa CapBank
#
# env.do: The Weboob command to call. It can take more than one argument.
#         With two argument, the second is used as parameter for the command.
#         The third is used to restrict backends.
# Example: env.do get_balance
#
# env.import: The import line to import the capabilities
# Example: from weboob.capabilities.bank import CapBank
#
# env.attribvalue: The attribut name of objects returned by the do command.
#                  For example, the "balance" member of Account objects
#                  If the attribut is itself one object, a hierarchical call can be done
#                  with the "/" operators.
# Example: env.attribvalue balance
# Example: env.attribvalue temp/value

## Optionals -- more configuration ##
# env.id_monitored: Restrict the results to a list of ids (space is used as separator)
# Example: env.id_monitored account1@backend1 account2@backend2
#
# env.exclude: Exclude some results (space is used as separator)
# Example: env.exclude 550810@sachsen
#
# env.cache_expire: To avoid site flooding, results are cached in folder
#                   /.config/weboob/munin/. The default lifetime of a cache value is 3600s
# Example: env.cache_expire 7200
#
# env.cumulate: Display data in Area mode (default) or in line mode.
# Example:  env.cumulate 0
#
# env.get_object_list: optional pre-call to get a list of objects, to applied
#                      the do function on it.
# Exemple: env.get_object_list="iter_subscriptions"
#
# env.attribid: Munin needs an id for each value. The default is to use the id of results,
#               but another attribute can be used. "/" can be used as separator for
#               hierarchical calls
# Example: env.attribid id
#
# env.title: A title for the graph (default: nothing)
# Example: env.title a wonderful graph
#
# env.vlabel: A vertical label for the graph
# Example: env.vlabel Balance
#
# env.label: Each data in munin as a label. Per default, the script takes the
#            "label" attribute of objects. However, it does not always exist,
#            and a better choice can be possible
# Example: env.label id
#
# env.category: set the graph category (default: weboob)
# Example: env.category bank
# For some running examples, see at the end of the script

from __future__ import print_function
Florent's avatar
Florent committed
import os
import sys
import locale
import time
import logging
from weboob.capabilities.base import NotAvailable
Florent's avatar
Florent committed
from weboob.core import Weboob, CallErrors
from weboob.exceptions import BrowserIncorrectPassword
Florent's avatar
Florent committed

Florent's avatar
Florent committed
class GenericMuninPlugin(object):
    def __init__(self):
        if 'weboob_path' in os.environ:
            self.weboob = Weboob(os.environ['weboob_path'])
        else:
            self.weboob = Weboob()
        self.cache_expire = long(os.environ.get('cache_expire', 3600))
        self.cumulate = int(os.environ.get('cumulate', 1))
        self.cache = None
        self.name = sys.argv[0]
        if "/" in self.name:
            self.name = self.name.split('/')[-1]

        # Capability to load
        self.capa = os.environ['capa']
        # Command to pass to Weboob
        self.do = os.environ['do'].split(',')
        # Not easy to load modules automatically...
        self.mimport = os.environ["import"]
        exec(self.mimport)
        # We can monitore only some objects
        self.object_list = None
        if 'get_object_list' in os.environ:
            self.object_list = os.environ["get_object_list"]
Florent's avatar
Florent committed
        self.tomonitore = None
        if 'id_monitored' in os.environ:
            self.tomonitore = os.environ['id_monitored'].decode('utf-8').split(' ')
Florent's avatar
Florent committed
        self.exclude = None
        if 'exclude' in os.environ:
            self.exclude = os.environ['exclude'].split(' ')
Florent's avatar
Florent committed
        # Attribut of object to use as ID (default: id)
        self.attribid = "id"
        if 'attribid' in os.environ:
            self.attribid = os.environ['attribid']
        self.attribvalue = os.environ['attribvalue']
        self.title = ''
        if 'title' in os.environ:
            self.title = os.environ['title'].decode('utf-8')
Florent's avatar
Florent committed
        self.attriblabel = "label"
        if 'label' in os.environ:
            self.attriblabel = os.environ['label']
        self.vlabel = self.attribvalue
        if 'vlabel' in os.environ:
            self.vlabel = os.environ['vlabel'].decode('utf-8')
        self.category = "weboob"
        if 'category' in os.environ:
            self.category = os.environ['category'].decode('utf-8')
Florent's avatar
Florent committed


    def display_help(self):
        print('generic-munin is a plugin for munin')
        print('')
        print('Copyright(C) 2013 Romain Bignon, Florent Fourcot')
        print('')
        print('To use it, create a symlink /etc/munin/plugins/nameyouwant to this script')
        print('and add this section in /etc/munin/plugin-conf.d/munin-node:')
        print('')
        print('[nameyouwant]')
        print('user romain')
        print('group romain')
        print('env.HOME /home/romain')
        print('# The weboob directory path.')
        print('env.weboob_path /home/romain/.config/weboob/')
        print('# Monitored objects. If this parameter is missing, all objects')
        print('# will be displayed.')
        print('env.id_monitored myid@backend1 otherid@backend2')
        print('# To prevent mass connections to websites, results are cached.')
        print('# You can set here the expiration delay (in seconds).')
        print('env.cache_expire 7200')
        print('# Cumulate values')
        print('env.cumulate 1')
        print('')
Florent's avatar
Florent committed

    def cachepath(self, name):
        tmpdir = os.path.join(self.weboob.workdir, "munin")
        if not os.path.isdir(tmpdir):
            os.makedirs(tmpdir)

        return os.path.join(tmpdir, name)

    def check_cache(self, name):
        return self.print_cache(name, check=True)

    def print_cache(self, name, check=False):
        try:
            f = open(self.cachepath(name), 'r')
        except IOError:
            return False

        try:
            last = int(f.readline().strip())
        except ValueError:
            return False

        if check and (last + self.cache_expire) < time.time():
            return False

        for line in f:
Florent's avatar
Florent committed
            sys.stdout.write(line)
        return True

    def new_cache(self, name):
        os.umask(0o077)
Florent's avatar
Florent committed
        new_name = '%s.new' % name
        filename = self.cachepath(new_name)
        try:
            f = open(filename, 'w')
        except IOError as e:
            print('Unable to create the cache file %s: %s' % (filename, e), file=sys.stderr)
Florent's avatar
Florent committed
            return

        self.cache = f
        self.cache.write('%d\n' % time.time())

    def flush_cache(self):
        old_name = self.cache.name
        new_name = self.cache.name[:-4]
        self.cache.close()
        os.rename(old_name, new_name)

    def write_output(self, line):
        sys.stdout.write('%s\n' % line)
        if self.cache:
            self.cache.write('%s\n' % line)

    def build_do(self):
        if self.object_list:
            results = []
            for result in self.weboob.do(self.object_list):
                results.append(result)
Florent's avatar
Florent committed
            for result in results:
Florent's avatar
Florent committed
                    for i in self.weboob.do(self.do[0], result.id, backends=result.backend):
                        yield i
                # Do not crash if one module does not implement the feature
                except CallErrors:
                    pass
        elif len(self.do) == 1:
            for i in self.weboob.do(self.do[0]):
                yield i
Florent's avatar
Florent committed
        elif len(self.do) == 2:
            for i in self.weboob.do(self.do[0], self.do[1]):
                yield i
Florent's avatar
Florent committed
        elif len(self.do) == 3:
            for i in self.weboob.do(self.do[0], self.do[1], backends=self.do[2]):
                yield i
    def get_value(self, result):
        attribs = self.attribvalue.split('/')
        for attrib in attribs:
            result = getattr(result, attrib)
            if type(result) is list:
                result = result[0]
        return result

    def monitored(self, result):
        id = self.result2weboobid(result)
Florent's avatar
Florent committed
        if self.exclude and id in self.exclude:
            return False
        return not self.tomonitore or id in self.tomonitore
    def result2weboobid(self, result):
        attribs = self.attribid.split('/')
        id = '%s@%s' % (getattr(result, attribs[0]), result.backend)
        return id

    def result2id(self, result):
        attribs = self.attribid.split('/')
        id = result
        for attrib in attribs:
            id = getattr(id, attrib)
        return '%s_%s' % (result.backend, id)


Florent's avatar
Florent committed
    def config(self):
        if self.check_cache('%s-config' % self.name):
            return

        self.new_cache('%s-config' % self.name)
        self.weboob.load_backends(self.capa)
        self.write_output('graph_title %s' % self.title.encode('iso-8859-15'))
        self.write_output('graph_vlabel %s' % self.vlabel.encode('iso-8859-15'))
Florent's avatar
Florent committed
        self.write_output('graph_category %s' % self.category)
        self.write_output('graph_args --rigid')
Camille Dehecq's avatar
Camille Dehecq committed
        if self.cumulate:
            self.write_output('graph_total Total')
Florent's avatar
Florent committed
        try:
            objects = []
            if self.tomonitore or self.exclude:
Florent's avatar
Florent committed
                d = {}
Florent's avatar
Florent committed
                for result in self.build_do():
Florent's avatar
Florent committed
                    if self.monitored(result):
                        d[self.result2weboobid(result)] = result

                if self.tomonitore:
                    for id in self.tomonitore:
                        try:
                            objects.append(d[id])
                        except KeyError:
                            pass
                else:
                    for id in d:
Florent's avatar
Florent committed
                        objects.append(d[id])
            else:
                objects = reversed([a for a in self.build_do()])
Florent's avatar
Florent committed

            first = True
            for result in objects:
                id = self.result2id(result)
                type = 'STACK'
                if first:
                    type = 'AREA'
                    first = False
                self.write_output('%s.label %s' % (id.encode('iso-8859-15'), getattr(result, self.attriblabel).encode('iso-8859-15')))
Florent's avatar
Florent committed
                if self.cumulate:
                    self.write_output('%s.draw %s' % (id, type))
        except CallErrors as errors:
Florent's avatar
Florent committed
            self.print_errors(errors)
            self.print_cache('%s-config' % self.name)
        else:
            self.flush_cache()

    def print_errors(self, errors):
        for backend, err, backtrace in errors:
            print((u'%s(%s): %s' % (type(err).__name__, backend.name, err)).encode(sys.stdout.encoding or locale.getpreferredencoding(), 'replace'), file=sys.stderr)
Florent's avatar
Florent committed
            if isinstance(err, BrowserIncorrectPassword):
                self.weboob.backends_config.edit_backend(backend.name, backend.NAME, {'_enabled': 'false'})
Florent's avatar
Florent committed

    def execute(self):
        if self.check_cache(self.name):
            return

        self.new_cache(self.name)
        self.weboob.load_backends(self.capa)
        try:
Florent's avatar
Florent committed
            for result in self.build_do():
Florent's avatar
Florent committed
                if self.monitored(result):
                    value = self.get_value(result)
                    if value is not NotAvailable:
                        self.write_output('%s.value %f' % (self.result2id(result).encode('iso-8859-15'), value))
        except CallErrors as errors:
Florent's avatar
Florent committed
            self.print_errors(errors)
            self.print_cache(self.name)
        else:
            self.flush_cache()

    def run(self):
        cmd = (len(sys.argv) > 1 and sys.argv[1]) or "execute"
        if cmd == 'execute':
            self.execute()
        elif cmd == 'config':
            self.config()
        elif cmd == 'autoconf':
            print('no')
Florent's avatar
Florent committed
            sys.exit(1)
        elif cmd == 'suggest':
            sys.exit(1)
        elif cmd == 'help' or cmd == '-h' or cmd == '--help':
            self.display_help()

        if self.cache:
            self.cache.close()

        sys.exit(0)

if __name__ == '__main__':
    logging.basicConfig()
    GenericMuninPlugin().run()

### Examples ###
## Like boobank-munin does
## Only for the example, you should use boobank-munin instead
#[bank]
#user florent
#group florent
#env.cache_expire 7200
#env.HOME /home/flo
#env.capa CapBank
#env.do iter_accounts
#env.import from weboob.capabilities.bank import CapBank
#env.attribvalue balance
#env.title Solde des comptes
#
#
## Balance of your leclercmobile subscription
#[leclercmobile]
#user florent
#group florent
#env.cache_expire 16800
#env.HOME /home/flo
#env.capa CapBill
#env.do get_balance,06XXXXXXXX,leclercmobile
#env.import from weboob.capabilities.bill import CapBill
#env.attribvalue price
#env.title Forfait leclercmobile
#env.vlabel Solde
#
#Result: http://fourcot.fr/weboob/leclercmobile-day.png
#
## Monitor water level in Dresden
#[leveldresden]
#user florent
#group florent
#env.cache_expire 7200
#env.HOME /home/flo
#env.capa CapGauge
#env.do get_last_measure,501060-level
#env.import from weboob.capabilities.gauge import CapGauge
#env.attribvalue level
#env.title Niveau de l'elbe
#env.label id
#
#
## The level of the elbe in all Sachsen's cities
#[levelelbesachsen]
#user florent
#env.cache_expire 800
#env.HOME /home/flo
#env.cumulate 0
#env.capa CapGauge
#env.do iter_gauges,Elbe,sachsen
#env.import from weboob.capabilities.gauge import CapGauge
#env.attribvalue sensors/lastvalue/level
#env.title Niveau de l'elbe en Saxe
#env.label name
#env.vlabel Hauteur du fleuve (cm)
#env.exclude 550810@sachsen
#
#Result: http://fourcot.fr/weboob/elbesachsen-day.png
#
## Temperature in Rennes
#[temprennes]
#user florent
#env.HOME /home/flo
#env.cumulate 0
#env.capa CapWeather
#env.do get_current,619163,yahoo
#env.import from weboob.capabilities.weather import CapWeather
#env.attribvalue temp/value
#env.attribid temp/id
#env.title Température à Rennes
#env.vlabel Température
#env.label id
#
#Result: http://fourcot.fr/weboob/temprennes-day.png