# $Id: cmon.py 691 2013-10-29 01:12:29Z mwall $
# Copyright 2013 Matthew Wall
"""weewx module that records cpu, memory, disk, and network usage.

Installation

Put this file in the bin/user directory.


Configuration

Add the following to weewx.conf:

[ComputerMonitor]
    database = computer_sqlite
    max_age = 2592000 # 30 days; None to store indefinitely

[Databases]
    ...
    [[computer_sqlite]]
        root = %(WEEWX_ROOT)s
        database = archive/computer.sdb
        driver = weedb.sqlite

[Engines]
    [[WxEngine]]
        service_list = ..., user.cmon.ComputerMonitor
"""

# FIXME: make these methods platform-independent instead of linux-specific
# FIXME: deal with MB/GB in memory sizes
# FIXME: save the counts or save the differences?  for now the differences

from __future__ import with_statement
import os
import platform
import syslog
import time

import weewx
from weewx.wxengine import StdService

def logmsg(level, msg):
    syslog.syslog(level, 'cmon: %s' % msg)

def logdbg(msg):
    logmsg(syslog.LOG_DEBUG, msg)

def loginf(msg):
    logmsg(syslog.LOG_INFO, msg)

def logerr(msg):
    logmsg(syslog.LOG_ERR, msg)

def get_int(config_dict, label, default_value):
    value = config_dict.get(label, default_value)
    if isinstance(value, str) and value.lower() == 'none':
        value = None
    if value is not None:
        try:
            value = int(value)
        except Exception, e:
            logerr("bad value '%s' for %s" % (value, label))
            value = default_value
    return value

def _readproc_line(filename):
    """read single line proc file, return the string"""
    info = ''
    with open(filename) as fp:
        info = fp.read()
    return info

def _readproc_lines(filename):
    """read proc file that has 'name value' format for each line"""
    info = {}
    with open(filename) as fp:
        for line in fp:
            line = line.replace('  ',' ')
            (label,data) = line.split(' ',1)
            info[label] = data
    return info

def _readproc_dict(filename):
    """read proc file that has 'name:value' format for each line"""
    info = {}
    with open(filename) as fp:
        for line in fp:
            if line.find(':') >= 0:
                (n,v) = line.split(':',1)
                info[n.strip()] = v.strip()
    return info

IGNORED_MOUNTS = [
    '/lib/init/rw',
    '/proc',
    '/sys',
    '/dev'
    ]

CPU_KEYS = ['user','nice','system','idle','iowait','irq','softirq']

# bytes received
# packets received
# packets dropped
# fifo buffer errors
# packet framing errors
# compressed packets
# multicast frames
NET_KEYS = [
    'rbytes','rpackets','rerrs','rdrop','rfifo','rframe','rcomp','rmulti',
    'tbytes','tpackets','terrs','tdrop','tfifo','tframe','tcomp','tmulti'
    ]

defaultSchema = [
    ('dateTime', 'INTEGER NOT NULL PRIMARY KEY'),
    ('usUnits', 'INTEGER'),
    ('mem_total','INTEGER'),
    ('mem_free','INTEGER'),
    ('mem_used','INTEGER'),
    ('swap_total','INTEGER'),
    ('swap_free','INTEGER'),
    ('swap_used','INTEGER'),
    ('cpu_user','INTEGER'),
    ('cpu_nice','INTEGER'),
    ('cpu_system','INTEGER'),
    ('cpu_idle','INTEGER'),
    ('cpu_iowait','INTEGER'),
    ('cpu_irq','INTEGER'),
    ('cpu_softirq','INTEGER'),
    ('load1','REAL'),
    ('load5','REAL'),
    ('load15','REAL'),
    ('proc_active','INTEGER'),
    ('proc_total','INTEGER'),

# the default interface on most linux systems is eth0
    ('net_eth0_rbytes','INTEGER'),
    ('net_eth0_rpackets','INTEGER'),
    ('net_eth0_rerrs','INTEGER'),
    ('net_eth0_rdrop','INTEGER'),
    ('net_eth0_tbytes','INTEGER'),
    ('net_eth0_tpackets','INTEGER'),
    ('net_eth0_terrs','INTEGER'),
    ('net_eth0_tdrop','INTEGER'),

# if the computer is an openvpn server, track the tunnel traffic
#    ('net_tun0_rbytes','INTEGER'),
#    ('net_tun0_rpackets','INTEGER'),
#    ('net_tun0_rerrs','INTEGER'),
#    ('net_tun0_rdrop','INTEGER'),
#    ('net_tun0_tbytes','INTEGER'),
#    ('net_tun0_tpackets','INTEGER'),
#    ('net_tun0_terrs','INTEGER'),
#    ('net_tun0_tdrop','INTEGER'),

# disk volumes will vary, but root is always present
    ('disk_root_total','INTEGER'),
    ('disk_root_free','INTEGER'),
    ('disk_root_used','INTEGER'),
#    ('disk_home_total','INTEGER'),
#    ('disk_home_free','INTEGER'),
#    ('disk_home_used','INTEGER'),
#    ('disk_var_weewx_total','INTEGER'),
#    ('disk_var_weewx_free','INTEGER'),
#    ('disk_var_weewx_used','INTEGER')
    ]

class ComputerMonitor(StdService):
    """Collect CPU, Memory, Disk, and other computer information."""

    def __init__(self, engine, config_dict):
        super(ComputerMonitor, self).__init__(engine, config_dict)

        d = config_dict.get('ComputerMonitor', {})

        # get the database parameters we need to function
        self.database = d['database']
        schema_str = d.get('schema', None)
        schema = weeutil.weeutil._get_object(schema_str) \
            if schema_str is not None else defaultSchema
        self.table = d.get('table', 'archive')

        # configure the database
        self.archive = weewx.archive.Archive.open_with_create(config_dict['Databases'][self.database], schema, self.table)

        # be sure database matches the schema we have
        dbcol = self.archive.connection.columnsOf(self.table)
        memcol = [x[0] for x in schema]
        if dbcol != memcol:
            raise Exception('schema mismatch: %s != %s' % (dbcol, memcol))

        # provide info about the platform on which we are running
        cpuinfo = _readproc_dict('/proc/cpuinfo')
        for key in cpuinfo:
            loginf('cpuinfo: %s: %s' % (key, cpuinfo[key]))

        self.max_age = get_int(d, 'max_age', 2592000)
        self.last_cpu = {}
        self.last_net = {}
        self.bind(weewx.NEW_ARCHIVE_RECORD, self.newArchiveRecordCallback)

    def shutDown(self):
        pass

    def newArchiveRecordCallback(self, event):
        """save data to database then prune old records as needed"""
        self.save_data(self.get_data())
        now = int(time.time())
        if self.max_age is not None:
            self.prune_data(now - self.max_age)

    def save_data(self, record):
        """save data to database"""
        self.archive.addRecord(record)

    def prune_data(self, ts):
        """delete records with dateTime older than ts"""
        sql = "delete from %s where dateTime < %d" % (self.table, ts)
        self.archive.getSql(sql)
        try:
            # sqlite databases need some help to stay small
            self.archive.getSql('vacuum')
        except Exception, e:
            pass

    def get_data(self):
        p = platform.system()
        if p == 'MacOSX':
            _method = self._get_macosx_info
        elif p == 'BSD':
            _method = self._get_bsd_info
        else:
            _method = self._get_linux_info
        record = {}
        record['dateTime'] = int(time.time()+0.5)  # required by weedb
        record['usUnits'] = weewx.US               # required by weedb
        record.update(_method())
        return record

    def _get_bsd_info(self):
        # FIXME: implement bsd methods
        return {}

    def _get_macosx_info(self):
        # FIXME: implement macosx methods
        return {}

    # this should work on any linux running kernel 2.2 or later
    def _get_linux_info(self):
        record = {}
        meminfo = _readproc_dict('/proc/meminfo')
        record['mem_total'] = int(meminfo['MemTotal'].split()[0]) # kB
        record['mem_free'] = int(meminfo['MemFree'].split()[0]) # kB
        record['mem_used'] = record['mem_total'] - record['mem_free']
        record['swap_total'] = int(meminfo['SwapTotal'].split()[0]) # kB
        record['swap_free'] = int(meminfo['SwapFree'].split()[0]) # kB
        record['swap_used'] = record['swap_total'] - record['swap_free']

        cpuinfo = _readproc_lines('/proc/stat')
        values = cpuinfo['cpu'].split()[0:7]
        for i,key in enumerate(CPU_KEYS):
            if self.last_cpu.has_key(key):
                record['cpu_'+key] = int(values[i]) - self.last_cpu[key]
            self.last_cpu[key] = int(values[i])

        netinfo = _readproc_dict('/proc/net/dev')
        for iface in netinfo:
            values = netinfo[iface].split()
            for i,key in enumerate(NET_KEYS):
                if not self.last_net.has_key(iface):
                    self.last_net[iface] = {}
                if self.last_net[iface].has_key(key):
                    record['net_'+iface+'_'+key] = int(values[i]) - self.last_net[iface][key]
                self.last_net[iface][key] = int(values[i])

#        uptimestr = _readproc_line('/proc/uptime')
#        (uptime,idletime) = uptimestr.split()

        loadstr = _readproc_line('/proc/loadavg')
        (load1,load5,load15,nproc) = loadstr.split()[0:4]
        record['load1'] = float(load1)
        record['load5'] = float(load5)
        record['load15'] = float(load15)

        (num_proc,tot_proc) = nproc.split('/')
        record['proc_active'] = int(num_proc)
        record['proc_total'] = int(tot_proc)

        disks = []
        mntlines = _readproc_lines('/proc/mounts')
        for mnt in mntlines:
            mntpt = mntlines[mnt].split()[0]
            ignore = False
            if mnt.find(':') >= 0:
                ignore = True
            for m in IGNORED_MOUNTS:
                if mntpt.startswith(m):
                    ignore = True
                    break
            if not ignore:
                disks.append(mntpt)
        for disk in disks:
            label = disk.replace('/','_')
            if label == '_':
                label = '_root'
            st = os.statvfs(disk)
            free = int((st.f_bavail * st.f_frsize) / 1024) # kB
            total = int((st.f_blocks * st.f_frsize) / 1024) # kB
            used = int(((st.f_blocks - st.f_bfree) * st.f_frsize) / 1024) # kB
            record['disk'+label+'_free'] = free
            record['disk'+label+'_total'] = total
            record['disk'+label+'_used'] = used

        return record



# what follows is a basic unit test of this module.  to run the test:
#
# cd /home/weewx
# PYTHONPATH=bin python bin/user/cmon.py
#
if __name__=="__main__":
    import time
    from weewx.wxengine import StdEngine
    config = {}
    config['Station'] = {}
    config['Station']['station_type'] = 'Simulator'
    config['Station']['altitude'] = [0,'foot']
    config['Station']['latitude'] = 0
    config['Station']['longitude'] = 0
    config['Simulator'] = {}
    config['Simulator']['driver'] = 'weewx.drivers.simulator'
    config['Simulator']['mode'] = 'simulator'
    config['ComputerMonitor'] = {}
    config['ComputerMonitor']['database'] = 'computer_sqlite'
    config['Databases'] = {}
    config['Databases']['computer_sqlite'] = {}
    config['Databases']['computer_sqlite']['root'] = '%(WEEWX_ROOT)s'
    config['Databases']['computer_sqlite']['database'] = '/tmp/computer.sdb'
    config['Databases']['computer_sqlite']['driver'] = 'weedb.sqlite'
    config['Engines'] = {}
    config['Engines']['WxEngine'] = {}
    config['Engines']['WxEngine']['service_list'] = 'user.cmon.ComputerMonitor'
    engine = StdEngine(config)
    svc = ComputerMonitor(engine, config)
    event = weewx.Event(weewx.NEW_ARCHIVE_RECORD)
    record = svc.get_data()
    print record

    time.sleep(5)
    record = svc.get_data()
    print record

    time.sleep(5)
    record = svc.get_data()
    print record

    os.remove('/tmp/computer.sdb')
