#!/s/bin/python -- # -*- python -*-
# $Id: kstats.py,v 1.8 2001-02-06 13:29:01-06 annis Exp $
# $Source: /u/annis/code/NewMom/kstat/RCS/kstats.py,v $
#
# Copyright (c) 1999-2001 William S. Annis.  All rights reserved.
# This is free software; you can redistribute it and/or modify it
# under the same terms as Perl (the Artistic Licence).  Developed at
# the Department of Biostatistics and Medical Informatics, University
# of Wisconsin, Madison.


"""User-friendly interface to the Solaris kstat(3k) library.

While this interface is more user-friendly than the SWIG interface to
kstat(3k), there are parts that aren't going to be useful to anyone
who doesn't know, say, the differences between NFS versions 2 and 3.

Recommended man pages to enhance understanding: netstat, nfsstat,
iostat.  The NFS and RPC statistics in particular are best understood
with help from the man page for nfsstat.

In order to limit the amount chain traversing going on, this API has a
sample rate.  Every query runs a check agains the last query time to
determine if the chain needs to be searched again.  The default sample
rate is 5 seconds.
"""

from kstat import Kstat
from time import time
import string


__kst = None                            # Kstat instance
__kst_ts = None                         # time-stamp of last chain update
__kst_sr = 5                            # sample rate, in seconds
__kst_watching = {}                     # dictionary of stats watched
__pti = None                            # /etc/path_to_inst data
__devdsk = None                         # links from /dev/dsk

class KstatsNoSuchDevice(Exception):
    pass

# This is very useful for information about disks.  The kstat interface
# still uses the SunOS names (sd0a, etc.), but us Solaris geeks like to
# see c0t3d0s0. 
# Kstat returns partition names like 'sd3,a', so that's what this expects.
def sd_to_ctds(sd):
    """Convert something like 'sd0,a' into 'c0t3d0s0'.

    This can also (attempt to) convert names such as 'sd0' into
    'c0t3d0' for full disks.
    """
    import os

    if __pti is None:
        # If this hasn't been intialized, neither has __devdsk...
        __read_devdsk()
        __read_path_to_inst()

    if sd[-2] == ",":
        part = ":" + sd[-1]
        sd = sd[:-2]
        partition = 1
    else:
        # There is no /dev/sd3, so fake out the disk name by asking
        # for the first slice, then chopping off that part of the name
        # later.
        part = ":a"
        partition = 0

    if __pti.has_key(sd):
        if partition:
            return __devdsk[__pti[sd] + part]
        else:
            return __devdsk[__pti[sd] + part][:-2]
    else:
        # Are you on the right machine?
        return sd

def ctds_to_sd(ctds):
    """Convert c0t0d0s0 into something like 'sd0,a'."""
    import os

    if ctds[-2] == 's':
        path = ctds
        partition = 1
    else:
        path = ctds + 's0'
        partition = 0

    try:
        devname = os.path.basename(os.readlink("/dev/dsk/" + path))
    except:
        raise KstatsNoSuchDevice, path

    # Now, turn the name of the device into something kstat knows about.
    part = devname[-1]
    (driver, no) = string.split(devname[:-2], "@")
    no = string.split(no, ",")[0]

    if partition:
        return driver + no + "," + part
    else:
        return driver + no

# __read_path_to_inst and __read_devdsk together generate dictionaries
# used by the current sd_to_ctds function.  /etc/path_to_inst is
# parsed and generates a dict keyed on driver + instance name.  So,
# an entry like this:
#
#     "/sbus@1f,0/SUNW,fas@e,8800000/sd@0,0" 0 "sd"
#
# and enter it into the dictionary as:
#
#    __pti['sd0'] = "/sbus@1f,0/SUNW,fas@e,8800000/sd@0,0"
#
# This is used with the dictionary of /dev/dsk/* names, which are all
# symbolic links to something that looks much like the value above.
# See sd_to_ctds for the chain of dictionary lookups.
# Some munching needs to be done to get information on an entire disk
# rather than just a partition.
def __read_path_to_inst():
    global __pti
    __pti = {}

    p = open("/etc/path_to_inst", "r")
    for line in p.readlines():
        if line[0] == "#": continue
        string.strip(line)
        (pname, inst, driver) = string.split(line)

        # Chop of leading and trailing quotes.
        pname = pname[1:-1]
        driver = driver[1:-1]

        __pti[driver + inst] = pname

    p.close()
    #print __pti, __devdsk

# This generates a full dictionary of /dev/dsk/* links.
def __read_devdsk():
    global __devdsk
    import glob, os

    __devdsk = {}
    for dsk in glob.glob('/dev/dsk/*'):
        dev = os.readlink(dsk)
        # Chop off the /dev/dsk and ../../devices grot, too.
        __devdsk[dev[13:]] = dsk[9:]

def __setup_kstat():
    global __kst, __kst_ts

    __kst = Kstat()
    __kst_ts = time() + __kst_sr

def __kstat_update(forced=0):
    """Update the Kstat instance if it's time to do so.

    An update can be forced with __kstat_update(forced=1).  Returns 1
    if there was an update, or 0 if not.
    """
    global __kst_ts

    if forced or time() > __kst_ts:
        __kst.update()
        __kst_ts = time() + __kst_sr
        return 1
    else:
        return 0

def set_sample_rate(sr):
    """Set the sample rate, in seconds.

    Resets the current sample time sensibly.
    """
    global __kst_ts, __kst_sr

    # Reset the time stamp first.
    __kst_ts = __kst_ts + (sr - __kst_sr)
    # Now change the sample rate.
    __kst_sr = sr

def get_sample_rate():
    return __kst_sr

def watching():
    """What statistics are being watched."""
    return __kst_watching.keys()

def is_watching(stat):
    """Is this measure being watched?

    if is_watching('load'):
        do_something_with_the_load()
    """
    return __kst_watching.has_key(stat)

# When you watch something new, there is a forced update of the chain
# to get the new data in.
def _watch(name_, module, instance, name, id=None):
    __kst_watching[name_] = 1
    __kst.watch(module, instance, name, id)
    __kstat_update(forced=1)

def load():
    """Current 1, 5 and 15 minute load averages."""
    if not is_watching('load'):
        _watch('load', 'unix', None, 'system_misc', id='load')

    __kstat_update()
    l = __kst.get('load')[0]

    return (l['avenrun_1min'] / 256.0,
            l['avenrun_5min'] / 256.0,
            l['avenrun_15min'] / 256.0)

def nprocs():
    """Number of current processes."""
    if not is_watching('load'):
        _watch('load', 'unix', None, 'system_misc', id='load')

    __kstat_update()
    l = __kst.get('load')[0]

    return l['nproc']

def boottime():
    """When the machine was last boot."""
    if not is_watching('load'):
        _watch('load', 'unix', None, 'system_misc', id='load')

    __kstat_update()
    l = __kst.get('load')[0]

    return l['boot_time']

def diskerrs(disk=None):
    """Get disk information and errors."""
    if not is_watching('sderr'):
        _watch('sderr', 'sderr', None, None, id='sderr')
        _watch('daderr', 'daderror', None, None, id='daderr')

    __kstat_update()

    dsks = []
    try:
        dsks = dsks + __kst.get('sderr')
        dsks = dsks + __kst.get('daderr')
    except:
        pass

    if disk:
        for d in dsks:
            if d.record[2] == "%s,err" % disk:
                return d

        raise KstatsNoSuchDevice, disk
    else:
        try:
            return __kst.get('sderr')
        except:
            return __kst.get('daderr')
diskinfo = diskerrs

# The statistics for RPC and NFS which come out of nfsstat are spread
# all over the kstat chain.  The cliches for extracting that information
# are homomorphic, however, and differ only in arguments to _watch.
def __rpcwrap(name_, mod, inst, name):
    if not is_watching(name_):
        _watch(name_, mod, inst, name, id=name_)

    __kstat_update()
    return __kst.get(name_)[0]

def rpc_client():
    """Get RPC client information and errors."""
    return __rpcwrap('rpc_client', 'unix', None, 'rpc_client')

def rpc_clts_client():
    """Get RPC (connectionless) client information and errors."""
    return __rpcwrap('rpc_clts_client', 'unix', None, 'rpc_clts_client')

def rpc_cots_client():
    """Get RPC (connection oriented) client information and errors."""
    return __rpcwrap('rpc_cots_client', 'unix', None, 'rpc_cots_client')

def rpc_server():
    """Get RPC server information and errors."""
    return __rpcwrap('rpc_server', 'unix', None, 'rpc_server')

def rpc_clts_server():
    """Get RPC (connectionless) server information and errors."""
    return __rpcwrap('rpc_clts_server', 'unix', None, 'rpc_clts_server')

def rpc_cots_server():
    """Get RPC (connection oriented) server information and errors."""
    return __rpcwrap('rpc_cots_server', 'unix', None, 'rpc_cots_server')

def nfs_server():
    """Get NFS server base information and errors."""
    return __rpcwrap('nfs_server', 'nfs', None, 'nfs_server')

def nfs_server_v2():
    """Get NFS.v2 server information and errors."""
    return __rpcwrap('nfs_server_v2', 'nfs', None, 'rfsproccnt_v2')

def nfs_server_v3():
    """Get NFS.v3 server information and errors."""
    return __rpcwrap('nfs_server_v3', 'nfs', None, 'rfsproccnt_v3')

def nfs_client():
    """Get NFS client base information and errors."""
    return __rpcwrap('nfs_client', 'nfs', None, 'nfs_client')

def nfs_client_v2():
    """Get NFS.v2 client information and errors."""
    return __rpcwrap('nfs_client_v2', 'nfs', None, 'rfsreqcnt_v2')

def nfs_client_v3():
    """Get NFS.v3 client information and errors."""
    return __rpcwrap('nfs_client_v3', 'nfs', None, 'rfsreqcnt_v3')

def nfs_acl_server_v2():
    """Get NFS.v2 ACL server information and errors."""
    return __rpcwrap('nfs_acl_server_v2', 'nfs_acl', None, 'aclproccnt_v2')

def nfs_acl_server_v3():
    """Get NFS.v3 ACL server information and errors."""
    return __rpcwrap('nfs_acl_server_v3', 'nfs_acl', None, 'aclproccnt_v3')

def nfs_acl_client_v2():
    """Get NFS.v2 ACL client information and errors."""
    return __rpcwrap('nfs_acl_client_v2', 'nfs_acl', None, 'aclreqcnt_v2')

def nfs_acl_client_v3():
    """Get NFS.v3 ACL client information and errors."""
    return __rpcwrap('nfs_acl_client_v32', 'nfs_acl', None, 'aclreqcnt_v3')

def cpuinfo(cpu=None):
    """Get CPU information.

    You can ask for info on a specific CPU by passing in the CPU number:

        info = cpuinfo(3)
    """
    if not is_watching('cpuinfo'):
        _watch('cpuinfo', 'cpu_info', None, None, id='cpuinfo')

    __kstat_update()

    if cpu:
        for c in __kst.get('cpuinfo'):
            if c.record[2] == "cpu_info%s" % cpu:
                return c

        # If this falls through, there's a problem.
        raise KstatsNoSuchDevice, "cpu %s" % cpu
    else:
        return __kst.get('cpuinfo')

def cpustat(cpu=None):
    """Get cpu_stat information.

    See /usr/include/sys/sysinfo.h for more information.

    You can ask for info on a specific CPU by passing in the CPU number:

        stat = cpustat(3)
    """
    if not is_watching('cpustat'):
        _watch('cpustat', 'cpu_stat', None, None, id='cpustat')

    __kstat_update()

    if cpu != None:
        try:
            return __kst.get('cpustat')[cpu]
        except:
            raise KstatsNoSuchDevice, "cpu %s" % cpu
    else:
        return __kst.get('cpustat')

def netstat(interface=None):
    """Get network statistics for all devices.

    You must ask for information about a specific device type:

        loopback = netstat('lo')
        netinfo = netstat('hme')

    Thus, if you have multiple interfaces, you'll get a list of answers.
    """
    watchname = 'netstat-' + interface
    if not is_watching(watchname):
        _watch(watchname, interface, None, None, id=watchname)

    __kstat_update()
    return __kst.get(watchname)

def diskstat(device):
    """Get IO statistics for a partition or disk.

    This will accept disk and parition names in either format: 'sd0'
    or 'c0t3d0' for a disk, and 'sd0,a' and 'c0t3d0s0' for a partition.
    """
    # Convert name to 'sd...' style format.
    if device[0] == 'c' and device[2] == 't':
        device = ctds_to_sd(device)

    watchname = "diskstat-" + device
    if not is_watching(watchname):
        _watch(watchname, None, None, device, id=watchname)

    __kstat_update()
    return __kst.get(watchname)

def vminfo():
    """Get information from the vminfo struct.

    See /usr/include/sys/sysinfo.h for struct details.
    """
    if not is_watching('vminfo'):
        _watch('vminfo', 'unix', None, 'vminfo', id='vminfo')

    __kstat_update()

    return __kst.get('vminfo')

def sysinfo():
    """Get information from the sysinfo struct.

    I'm not sure how useful this is.  It has only a fiew fields
    dealing with swapping and runqueue stuff.

    See /usr/include/sys/sysinfo.h for struct details.
    """
    if not is_watching('sysinfo'):
        _watch('sysinfo', 'unix', None, 'sysinfo', id='sysinfo')

    __kstat_update()

    return __kst.get('sysinfo')

def ncstats():
    """Get information from the ncstats struct.

    See /usr/include/sys/dnlc.h for struct details.

    Name lookup information can be found on page 360 of 'Sun Performance
    and Tuning.'
    """
    if not is_watching('ncstats'):
        _watch('ncstats', 'unix', None, 'ncstats', id='ncstats')

    __kstat_update()

    return __kst.get('ncstats')


# Initialize __kst
__setup_kstat()



# EOF