#!/usr/bin/python2 -OO
#
# Time-stamp: <2004-12-29 13:22:31 hcz>
"""
Python program for displaying network usage summaries

Examines the logfiles created by netcount-upd and outputs a
formatted summary.

Written by Heike C. Zimmerer <hcz@hczim.de>
"""

version = "netcount v0.8i 2004-12-29 hcz"

# version history: See ChangeLog and README


#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; version 2 of the License.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#



import getopt, sys, os, glob, gzip, time, ConfigParser, re
pname = os.path.basename(sys.argv[0])

def usage():
    print """
Usage: %s [options] <datespec>
Function: Display ppp device traffic summaries
Argument: <datespec>: Starting date (Example: -22 days, 1 week ago, start
          (of current accounting period), start -1 month, -2weeks.
          (Current setting: %s)
Options:
  -e, --end <datespec>    specify ending date.  See "argument", above,
                          for examples.  Default: now.
  -a, --all               show all records, independent of date
  -r, --records <rspec>   select records to display. rspec is one or more
                          of c, h, d, D, m, M, t, for per-call, hourly, daily,
                          daily percentage, monthly, monthly percentage
                          and totals lines, respectively.
                          (current setting: -r %s)
  -c. --columns <cspec>   select columns to list. cspec is one or more of w, b,
                          t, l, s for weekday, bytecount, transmission speed,
                          length, and separating Rx and Tx, respectively.
                          (Current setting: -c %s)
  -o, --round-up          round up to the next full minute on hangup
                          (current setting: %s)
  -u, --bytes-unit k|M|G  display unit for Byte counts is kB, MB or GB
                          (current setting: -u %s)
  -s, --speed-unit k|M|G  display unit for transfer speed is kB/s, MB/s or GB/s
                          (current setting: -s %s)
  -i, --binary            toggle display unit from 2**10 to 10**3 and vice
                          versa  (e.g.,MB vs. MiB) (current setting: %s)
  -t, --time-unit s|m|h   time unit: show times as seconds, minutes or hours
                          (current setting: -t %s)
  -D, --deadline <day>    set ending day for the monthly summaries
                          (current setting: -D %s)
  -B, --bytes-limit <count>  total traffic, 100%% mark. count may be followed
                          by unit (e.g. G, GB, GiB)
                          (current setting = %s)
  -R, --rx-limit <count>  rx traffic, 100%% mark (curr. setting = -R %s)
  -T, --tx-limit <count>  tx traffic, 100%% mark (curr. setting = -T %s)
  -L, --time-limit <tim>  time, 100%% mark, in hours. May be followed by s,
                          m, min, or h. (current setting: -L %s)
  -l, --logfiles <path>   specify path to the logfiles (shell glob pattern)
                          (current setting: -l %s)
  -f, --read-file <fname> read additional configuration from <fname>
  -w, --write-config      write configuration to ~/.netcountrc
  -W, --write-file <fname> write configuration to <fname>
  -V, --version           print version info and exit
  -h, --help              display this help

examples:
  netcount 2 days ago        show last 2 days
  netcount -2 weeks          show last 2 weeks
  netcount start             show last accounting period
  netcount -a -r chdmt       for an overall display
  netcount -r dm             show daily and monthly traffic
  netcount -r c yesterday    show calls since yesterday
""" % (pname,
       config_opts["display"]["begin"],
       config_opts["display"]["records"],
       config_opts["display"]["columns"],
       ("off", "on")[config_opts["accounting"]["round-up"] == "1"],
       config_opts["display"]["bytes-unit"],
       config_opts["display"]["speed-unit"],
       ("10**3", "2**10")[config_opts["display"]["binary"] == "1"],
       config_opts["display"]["time-unit"],
       config_opts["accounting"]["deadline"],
       config_opts["accounting"]["bytes-limit"],
       config_opts["accounting"]["rx-limit"],
       config_opts["accounting"]["tx-limit"],
       config_opts["accounting"]["time-limit"],
       config_opts["files"]["logfiles"])
    if __debug__:
        print """Debugging option:
    -v, --verbose <cdrfs>    output debugging info"""

# Predefined values
# Format: { Section : { option : value, ...}, ...}
config_opts = {
                "display" : {
                    "bytes-unit" : "M",
                    "binary" : "0",
                    "time-unit" : "h",
                    "speed-unit" : "k",
                    "columns" : "bstl",
                    "records" : "chdm",
                    "begin" : "yesterday"
                },
                "accounting" : {
                    "deadline" : "31",
                    "bytes-limit" : "0",
                    "rx-limit" : "0",
                    "tx-limit" : "0",
                    "time-limit" : "0h",
                    "round-up" : "0"
                },
                "files": {
                    "logfiles" : "/var/log/netcount/netcount.log*"
                }
}


def intersect(q1, q2):
    return[ x for x in q1 if x in q2]


class ConfigObject(ConfigParser.ConfigParser):
    # initialize default config sections and config options:
    def __init__(self):
        # initalize with "hardwired" defaults:
        ConfigParser.ConfigParser.__init__(self)
        for sect in config_opts:
            self.add_section(sect)
            for opt in config_opts[sect]:
                self.set(sect, opt, config_opts[sect][opt])

    def read_config(self, fname = ""):
        # read Config file(s):
        try:
            if fname:
                try:
                    f = open(os.path.expanduser(fname))
                except Exception, e:
                    fatalerr(str(e))
                self.readfp(f)
            else:
                self.read(["/etc/netcountrc",\
                           os.path.expanduser("~/.netcountrc")])
        except ConfigParser.ParsingError, e:
            fatalerr(str(e))
        # check new contents:
        for sect in self.sections():
            if sect not in config_opts:
                err.inc("*** Unknown section in config file: '%s'" % sect)
                self.remove_section(sect)
            else:
                for opt in self.options(sect):
                    if opt in ("ieee802", ):
                        perr("Info: Obsolete option in config file: %s" % opt,
                             "run 'netcount -w' to correct")
                    elif opt not in config_opts[sect]:
                        err.inc("*** Unknown option in section %s in config file: '%s'" % (sect, opt))
                        self.remove_option(sect, opt)
                    else:
                        set_option("--" + opt, self.get(sect, opt))
                        
    def write_config(self, fnam):
        try:
            if fnam == "-":
                file = sys.stdout
            else:
                file = open(fnam, "w")
            for sect in config_opts:
                file.write("[%s]\n" % sect)
                for opt in config_opts[sect]:
                    file.write("%s = %s\n" % (opt, config_opts[sect][opt]))
                file.write("\n")
        except Exception, e:
            fatalerr(str(e))
        if fnam != "-":
            print("%s: Configuration written to %s." % (pname, fnam))
        

def perr(*s):
    for item in s:
        sys.stderr.write(str(item))
        sys.stderr.write("\n")

def fatalerr(*s):
    perr(*s)
    sys.exit(1)

def abort():
    fatalerr("Abort...")

class NonFatalErr:
    def __init__(self):
        self.errors = 0
    def inc(self, *s):
        perr(*s)
        self.errors += 1
        if self.errors > 10:
            fatalerr(pname + ": Too many errors, aborting...")
            
err = NonFatalErr()

class DateList(list):
    """A list whose elements correspond to a time tuple, augmented by
    several methods"""
    
    def __init__(self, l = [1900, 1, 1, 0, 0, 0, 0, 1, -1]):
        if type(l) == type(""):
            self.user_set(l)
        else:
            self[:] = l

    max_day = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
    def end_of_month(self):
        end_of_month = DateList.max_day[self[1]]
        if end_of_month == 28:          # February
            end_of_month += (self[0] % 4) == 0 # 29 if leap year
        return end_of_month
        
    def adjust_for_end_of_month(self):
        # if day is past the end of month, fill ein end of month
        if self[2] > self.end_of_month:
            self[2] = self.end_of_month # correct day if too large

    def start_of_day(self):
        d = DateList(self)
        d[3:7] = [0, 0, 0, 1]        # zero out time of day fields
        return d
        
    def user_setOld(self, ymd, default_month = "1", default_day = "1"):
        """The "old" method (up to version 0.7) of date inputting.
        Convert user input (string, "y-m-d") into DateList (self).
        If day omitted: 
          default_day > "30" means insert the last day of the month,
          else insert default_day (string)
        returns 1 if valid date found, None else.
        """
        strings = ymd.split("-")
        if not 2 <= len(strings) <= 3:
            return None
        # insert left-out arguments:
        if len(strings) == 2:
            strings.append(default_day)    # add day element
        if strings[0] == "":               # check year
            strings[0] = time.strftime("%Y")
            default_month = time.strftime("%m") # use current month as default
            default_day = time.strftime("%d")   # and current day
        if strings[1] == "":               # check month
            strings[1] = default_month
        if strings[2] == "":               # check day
            strings[2] = default_day
        # convert to integer:
        try:
            self[:] = DateList([int(s) for s in strings] + [0, 0, 0, 0, 1, -1])
        except ValueError:
            return None
        return 1

    def user_set(self, ymd, default_month = "1", default_day = "1"):
        # Interpret user date (string, ymd).  First, it is checked for
        # "old style" format (v0.7f and earlier).  If no valid date
        # can be found, the string is fed to the date utility with the
        # word "start" (if found) replaced by the starting date of the
        # current accounting period.  If the date command returns no
        # valid date, its output is printed as error message and the
        # program exits.
        orig_ymd = ymd                  # save for error msg
        # now try old style date:
        if not self.user_setOld(ymd, default_month, default_day):
            # not "old" style date. Try GNU date:
            if __debug__:
                if "d" in vopt: print "trying 'date' utility"
            first = DateList()
            first.user_set("--")        # set to current date and then...
            first = first.start_of_deadline() # to start of accounting period
            ymd = ymd.replace("start", first.ymd_string())
            dat = os.popen(
                "date --iso-8601 -d '%s' 2>&1" % ymd
                ).read().rstrip()
            if __debug__:
                if "d" in vopt: print "'date' returned %s" % dat
            try:
                self.string_set(dat)
            except:                     # date didn't return a date
                fatalerr(dat.replace("date: ","")) # print err msg and exit
        # check and correct values:
        if self[0] < 1990:         # allow logs before 2000
            if self[0] > 90:
                self += 1900
            else:
                self[0] += 2000    # 2000 omitted. Add it.
        if not (1990 <= self[0] <= 2099 and 1 <= self[1] <= 12 \
               and 1 <= self[2] <= 31):
            fatalerr("Impossible date (out of range): %s" % orig_ymd)
        self.adjust_for_end_of_month()
        if __debug__:
            if "d" in vopt: print "end user_set(). date list:", self

    def string_set(self, ymd, hms="0:0:0"):
        """like user_set, but requires the date string to be complete"""
        self[:] = list(time.strptime("%s %s" % (ymd, hms),
                                       "%Y-%m-%d %H:%M:%S"))
        
    def next_month(self):
        """same day, next month"""
        self[1] += 1               # next month
        if self[1] > 12:
            self[0] += 1
            self[1] = 1

    def next_deadline(self):
        """returns the next date (including or after self)
        with day matching option ..["deadline"]"""
        d = DateList(self)
        d[2] = int(config_opts["accounting"]["deadline"])
        if d < self:                    # current day of month past deadline
            d.next_month()
        d[3:6] = [0, 0, 0]
        d.adjust_for_end_of_month()
        return d

    def prv_deadline(self):
        """return the last day of the previous accounting period"""
        d = self.start_of_day()
        new = DateList(d)
        new[2] = int(config_opts["accounting"]["deadline"])
        if new >= d:
            new[1] -= 1                   # previous month
            if new[1] < 1:
                new[0] -= 1
                new[1] = 12
        new.adjust_for_end_of_month()
        return new

    def start_of_deadline(self):
        """return DateList object of the starting date of the current
        deadline period (i.e.  the day after the last deadline) as a
        DateList object"""
        d = self.prv_deadline()
        if d[2] >= d.end_of_month:       # if past end of month:
            d.next_month()              # next one
            d[2] = 1
        return d
        
    def wmd_string(self):
        return time.strftime("%a -%02m-%02d", self)

    def ym_string(self):                # return year and month as string
        return time.strftime("%Y-%02m", self)

    def ymd_string(self):
        return time.strftime("%Y-%02m-%02d", self)

    def hms_string(self):
        return time.strftime("%2H:%02M:%02S", self)

    def ymdhms_string(self):
        return time.strftime("%Y-%02m-%02d %2H:%02M:%02S", self)

#------------------------------------------ Command line parsing
def time_set(s):
    try:
        m = re.match(r"([\d.e+]+)([hms])?((?<=m)in)?$", s, re.IGNORECASE
                     ).groups()
        factor = 1
        if m[1]:
            factor =  {"S" : 1, "M" : 60, "H" : 3600}[m[1].upper()]
        res = int(float(m[0]) * factor)
    except Exception, e:
        fatalerr("illegal time value: " + s, e)
    return res

def bytecountSet(s):
    try:
        m = re.match("([\d.e+]+)([kMG]?i?)?b?$", s, re.IGNORECASE
                       ).groups()
        factor = 1
        if m[1]:
            factor = { "K" : 10L**3, "KI": 2L**10, "M" : 10L**6, "MI" : 2L**20,
                       "G" : 10L**9, "GI" : 2L**30 }[m[1].upper()]
        res = long(float(m[0]) * factor)
    except:
        fatalerr("illegal byte count value: " + s)
    return res
    
def bunit():
    """ Returns the byte count unit according to the option settings """
    budicts = ( { "b" : "B  ", "k" : "kB ", "M" : "MB ", "G" : "GB " }, \
                { "b" : "B  ", "k" : "kiB", "M" : "MiB", "G" : "GiB" } )
    return budicts[config_opts["display"]["binary"] == "1"] \
           [config_opts["display"]["bytes-unit"]]

def sunit():
    """ Returns the speed unit according to the option settings """
    sudicts = ({ "b" : "B/s  ", "k" : "kB/s ", "M" : "MB/s ", "G" : "GB/s " },
               { "b" : "B/s  ", "k" : "kiB/s", "M" : "MiB/s", "G" : "GiB/s" })
    return sudicts[config_opts["display"]["binary"] == "1"] \
           [config_opts["display"]["speed-unit"]]

bibdicts = ( { "b" : 1., "k" : 1.e3,   "M" : 1.e6,   "G" : 1.e9 }, \
             { "b" : 1., "k" : 2.**10, "M" : 2.**20, "G" : 2.**30 } )

def tobib(bytes):
    """ convert bytecount value for display into units according to
    config_opts["display"]["bytes-unit"] """
    return "%10.3f" % (bytes / bibdicts\
                       [config_opts["display"]["binary"] == "1"]
                       [config_opts["display"]["bytes-unit"]])

def tosu(bytes):
    """ convert speed value for display into units according to
    config_opts["display"]["speed-unit"] """
    return "%8.2f" % (bytes / \
                      bibdicts[config_opts["display"]["binary"] == "1"]\
                      [config_opts["display"]["speed-unit"]])


def totim(sec):
    """ convert time value (seconds) """
    if "s" in config_opts["display"]["time-unit"]:
        return "%10ds" % sec
    if "m" in config_opts["display"]["time-unit"]:
        return "%5d:%02dmin" % (sec / 60, sec % 60)
    if "h" in config_opts["display"]["time-unit"]:
        return "%4d:%02d:%02dh" % (sec / 3600, (sec /60) % 60, sec % 60)
    return None

def check_cset_args(value, description, cset, length = 99):
    """Check if option argument is a member of character set cset.
    Print error message and abort if not"""
    if len(value) > length:
        fatalerr("max. %d character%s allowed as a %s option argument: %s" % \
                 (length, ("s", "")[length == 1], description, value))
    for c in value:
        if c not in cset:
            fatalerr("not allowed as a %s option argument: %s\n"
                     "valid characters are: %s" % \
                     (description, c, cset))

fopt = vopt = wopt = copt = ropt = ""
bytes_limit = tx_limit = rx_limit = time_limit = 0
end = DateList("now")

def set_option(option, value):
    global wopt, vopt, begin, end, fopt, copt, ropt, config_opts
    global time_limit, bytes_limit, rx_limit, tx_limit
    
    if option in ("-h", "--help"):
        usage()
        sys.exit(0)
    elif option in ("-a", "--all"):
        begin.user_set("1990-")
    elif option in ("-c", "--columns"):
        check_cset_args(value, "-c", "bstlw")
        config_opts["display"]["columns"] = value
        copt = value
    elif option in ("-r", "--records"):
        check_cset_args(value, "-r", "chdDmMt")
        config_opts["display"]["records"] = value
        ropt = value
    elif option in ("-i", "--binary"):
        if value:                       # called from read_config()
            config_opts["display"]["binary"] = value
        else:                           # user input: toggle
            config_opts["display"]["binary"] = \
                      ["1", "0"][config_opts["display"]["binary"] == "1"]
    elif option in ("-o", "--round-up"):
        if value:                       # called from read_config()
            config_opts["accounting"]["round-up"] = value
        else:                           # user input: toggle
            config_opts["accounting"]["round-up"] = \
                      ["1", "0"][config_opts["accounting"]["round-up"] == "1"]
    elif option in ("-V", "--version"):
        print version
        sys.exit(0)
    elif option in ("-l", "--logfiles"):
        config_opts["files"]["logfiles"] = value
    elif option in ("-D", "--deadline"):
        config_opts["accounting"]["deadline"] = value
    elif option in ("-L", "--time-limit"):
        config_opts["accounting"]["time-limit"] = value
        time_limit = time_set(value)
    elif option in ("-B", "--bytes-limit"):
        config_opts["accounting"]["bytes-limit"] = value
        bytes_limit = bytecountSet(value)
    elif option in ("-R", "--rx-limit"):
        config_opts["accounting"]["rx-limit"] = value
        rx_limit = bytecountSet(value)
    elif option in ("-T", "--tx-limit"):
        config_opts["accounting"]["tx-limit"] = value
        tx_limit = bytecountSet(value)
    elif option in ("-v", "--verbose"):
        if not __debug__: fatalerr("Option -v not recognized")
        check_cset_args(value, "-v", "cdrfs")
        vopt = value
    elif option in ("-u", "--bytes-unit"):
        check_cset_args(value, "-u", "kMG", 1)
        config_opts["display"]["bytes-unit"] = value
    elif option in ("-t", "--time-unit"):
        check_cset_args(value, "-t", "hms", 1)
        config_opts["display"]["time-unit"] = value
    elif option in ("-s", "--speed-unit"):
        check_cset_args(value, "-s", "kMG", 1)
        config_opts["display"]["speed-unit"] = value
    elif option in ("-b", "--begin"):
        begin = DateList(value)
        config_opts["display"]["begin"] = value
    elif option in ("-e", "--end"):
        end.user_set(value, "12", "31")
    elif option in ("-f", "--read-file"):
        cfg.read_config(value)          # read in additional configuration
    elif option in ("-w", "--write-config"):
        wopt = "~/.netcountrc"
    elif option in ("-W", "--write-file"):
        wopt = value
    else:
        fatalerr("Internal error: set_option(), option '%s', value '%s'" %\
                 (option, value))

#---------------------------------------------------------------------
# The following is a (slightly modified) copy of getopt.py, Python2.2.
# Long option support added by Lars Wirzenius <liw@iki.fi>.
# Gerrit Holl <gerrit@nl.linux.org> moved the string-based exceptions
# to class-based exceptions.

__all__ = ["GetoptError","error","getopt"]

class GetoptError(Exception):
    opt = ''
    msg = ''
    def __init__(self, msg, opt):
        self.msg = msg
        self.opt = opt
        Exception.__init__(self, msg, opt)

    def __str__(self):
        return self.msg

error = GetoptError # backward compatibility

def getopt(args, shortopts, longopts = []):
    # This is a modified version of the Python 2.2 library getopt.
    #
    # Addition: A dash followed by a digit denotes end of options, so
    # it is considered first part of the argument, not an option.  This
    # facilitates date entry ("-2 days").  Also, "--" is considered an
    # argument (stands for "now" in old date syntax) instead of meaning
    # end of options and being ignored elsewhere.
    opts = []
    while args and re.match(r"-([^-\d]|-.)", args[0]):
        if args[0].startswith('--'):
            opts, args = do_longs(opts, args[0][2:], longopts, args[1:])
        else:
            opts, args = do_shorts(opts, args[0][1:], shortopts, args[1:])
    return opts, args

def do_longs(opts, opt, longopts, args):
    try:
        i = opt.index('=')
    except ValueError:
        optarg = None
    else:
        opt, optarg = opt[:i], opt[i+1:]

    has_arg, opt = long_has_args(opt, longopts)
    if has_arg:
        if optarg is None:
            if not args:
                raise GetoptError('option --%s requires argument' % opt, opt)
            optarg, args = args[0], args[1:]
    elif optarg:
        raise GetoptError('option --%s must not have an argument' % opt, opt)
    opts.append(('--' + opt, optarg or ''))
    return opts, args

# Return:
#   has_arg?
#   full option name
def long_has_args(opt, longopts):
    possibilities = [o for o in longopts if o.startswith(opt)]
    if not possibilities:
        raise GetoptError('option --%s not recognized' % opt, opt)
    # Is there an exact match?
    if opt in possibilities:
        return 0, opt
    elif opt + '=' in possibilities:
        return 1, opt
    # No exact match, so better be unique.
    if len(possibilities) > 1:
        # XXX since possibilities contains all valid continuations, might be
        # nice to work them into the error msg
        raise GetoptError('option --%s not a unique prefix' % opt, opt)
    assert len(possibilities) == 1
    unique_match = possibilities[0]
    has_arg = unique_match.endswith('=')
    if has_arg:
        unique_match = unique_match[:-1]
    return has_arg, unique_match

def do_shorts(opts, optstring, shortopts, args):
    while optstring != '':
        opt, optstring = optstring[0], optstring[1:]
        if short_has_arg(opt, shortopts):
            if optstring == '':
                if not args:
                    raise GetoptError('option -%s requires argument' % opt,
                                      opt)
                optstring, args = args[0], args[1:]
            optarg, optstring = optstring, ''
        else:
            optarg = ''
        opts.append(('-' + opt, optarg))
    return opts, args

def short_has_arg(opt, shortopts):
    for i in range(len(shortopts)):
        if opt == shortopts[i] != ':':
            return shortopts.startswith(':', i+1)
    raise GetoptError('option -%s not recognized' % opt, opt)
#------------------------------------------------------- end of getopt code


def cmdargs():
    """
    deal with command line arguments
    """
    try:
        opts, args = getopt(sys.argv[1:],
                     "hv:b:e:r:u:t:aiVl:c:s:wW:D:T:B:f:UoR:L:",
                     ["help", "verbose=", "begin=", "end=",
                      "records=", "bytes-unit=", "time-unit=", "all",
                      "binary", "version", "files=",
                      "columns=", "speed-unit=", "write-config",
                      "write-file=", "deadline=", "time-limit=",
                      "bytes-limit=", "read-file=", "round-up"
                      "rx-limit=", "tx-limit="])
    except GetoptError, e:
        perr(e, "Use the -h or --help options for help on command line syntax")
        sys.exit(2)
    for option, value in opts:
        set_option(option, value)
    if args:
        set_option("--begin", " ".join(args))

def check_limits():
    global bytes_limit, rx_limit, tx_limit
    """ checks bytes-, rx- and tx-limit """
    if bytes_limit and tx_limit and rx_limit:
        if bytes_limit != rx_limit + tx_limit:
            err.inc("Warning: bytes-limit does not equal sum of tx-limit and"
                    "rx-limit")
    elif not bytes_limit and tx_limit and rx_limit:
        bytes_limit = tx_limit + rx_limit
    elif bytes_limit and tx_limit:
        rx_limit = bytes_limit - tx_limit
    elif bytes_limit and rx_limit:
        tx_limit = bytes_limit - rx_limit
    

def percent_string(numerator, denominator):
    if not denominator:
        return ""
    percent = 100. * numerator / denominator
    if percent > 9999.99:
        percent = 9999.99
    return "%7.2f%%" % percent
    
class StatRecord:
    # StatRecords are used to accumulate the statistics data.  The
    # field "delim" denotes the type of record (per connection, daily,
    # ...) and at the same time is the delimiter that is printed out
    # before the line in the listing.

    # initialize record. delim is the character the separating line before
    # the printed summary consists of.
    def __init__(self, delim):
        self.delim = delim
        self.start_date = DateList()
        self.current_date = DateList()
        self.reset()

    def reset(self, date_l = DateList()):
        self.start_date[:] = date_l
        self.len = 0
        self.rxb = 0l
        self.txb = 0l
        self.has_data = 0

    def set(self, current_date, len, rxb, txb):
        self.current_date = current_date
        self.len = len
        self.rxb = rxb
        self.txb = txb
        self.has_data = 1

    def addrec(self, srec):
        self.current_date = srec.current_date
        self.len += srec.len
        self.rxb += srec.rxb
        self.txb += srec.txb
        self.has_data = 1

    lastdelim = ""
    hyphens = 0
    
    def prt(self, title = ""):
        if self.delim != StatRecord.lastdelim and StatRecord.hyphens:
            print self.delim * StatRecord.hyphens
        StatRecord.lastdelim = self.delim
        self.sum = self.rxb + self.txb
        if self.len == 0:
            self.len = 1                # prevent division by zero
        if title:
            s = "%-10s " % title
        elif self.delim == "=":         # monthly record. Omit day.
            s = "%-10s " % self.start_date.ym_string()
        elif "w" in copt:               # insert ww mm-dd
            s = self.start_date.wmd_string() + " "
        else:                           # Insert yyyy-mm-dd
            s = self.start_date.ymd_string() + " "

        if intersect("ch", ropt):
            if self.delim == "":
                s += "%8s " % self.start_date.hms_string()
            else:
                s += " " * 9
        if "s" in copt:
            if "b" in copt:
                s += "%s %s%s " % (tobib(self.rxb), tobib(self.txb), bunit())
            if "t" in copt:
                s += "%s %s%s " % \
                     (tosu(self.rxb / self.len), \
                      tosu(self.txb / self.len), \
                      sunit())
        else:
            if "b" in copt:
                s += "%s%s " % (tobib(self.sum), bunit())
            if "t" in copt:
                s += "%s%s " % (tosu(self.sum / self.len), sunit())
        if "l" in copt:
            s += totim(self.len)
        s = s.rstrip()                  # remove trailing spaces
        StatRecord.hyphens = len(s)
        try:
            print s
        except Exception, e:            # no stack trace on errors, ..
            fatalerr(str(e))            # .. e.g.  on broken pipe

    def prt_percent(self):
        if self.delim != StatRecord.lastdelim and StatRecord.hyphens:
            print self.delim * StatRecord.hyphens
        StatRecord.lastdelim = self.delim
        # calculate amounts correspondig to 100%
        start_of_period = self.start_date.prv_deadline()
        end_of_period = self.start_date.next_deadline()
        start_sec = time.mktime(start_of_period)
        period_secs = time.mktime(end_of_period) - start_sec
        if self.delim == "-":           # "daily" record type
            if "d" in ropt:             # date already printed, so..
                s = " " * 11            # .. keep first column empty
            else:
                if "w" in copt:
                    s = "%-10s " % self.start_date.wmd_string()
                else:
                    s = "%-10s " % self.start_date.ymd_string()
            days_per_period = period_secs / 86400
            d_time_limit = time_limit / days_per_period
            time_percent = percent_string(self.len, d_time_limit)
            d_bytes_limit = bytes_limit / days_per_period
            traffic_percent = percent_string(
                               self.rxb + self.txb, d_bytes_limit)
            d_rx_limit = rx_limit / days_per_period
            rx_percent = percent_string(self.rxb, d_rx_limit)
            d_tx_limit = tx_limit / days_per_period
            tx_percent = percent_string(self.txb, d_tx_limit)
        else:                           # "monthly" record type
            current_secs = time.mktime(self.current_date.start_of_day())\
                           - start_sec
            if "M" in ropt:             # date already printed, so..
                s = "%10s " %  percent_string(current_secs, period_secs)
            else:
                s = "%-10s " % self.start_date.ym_string()
            time_percent = percent_string(self.len, time_limit)
            traffic_percent = percent_string(
                               self.rxb + self.txb, bytes_limit)
            rx_percent = percent_string(self.rxb, rx_limit)
            tx_percent = percent_string(self.txb, tx_limit)
        
        
        if intersect("ch", ropt):
            s += " " * 9
        if "s" in copt:
            if "b" in copt:
                if rx_limit or tx_limit:
                    s += "%10s  %11s " % (rx_percent, tx_percent)
                else:
                    s += "    %11s          " % traffic_percent
                  
            if "t" in copt:
                s += " " * 23
        else:
            if "b" in copt:
                s += "%12s  " % traffic_percent
            if "t" in copt:
                s += " " * 14
        if "l" in copt:
            s += "%11s" % time_percent
        try:
            print s
        except Exception, e:            # no stack trace on errors, ..
            fatalerr(str(e))            # .. e.g.  on broken pipe
    
    def prt_header(self):
        s = "%-10s " % "Date"
        if intersect("ch", ropt):
            s += "%8s " % "Time"
        if "s" in copt:
            if "b" in copt:
                s += "%10s  %11s " % ("Rx Bytes", "Tx Bytes")
            if "t" in copt:
                s += "%8s %8s     " % ("Rx", "Tx")
        else:
            if "b" in copt:
                s += "%11s   " % "Bytes"
            if "t" in copt:
                s += "%12s " % "speed"
        if "l" in copt:
            s += "      Length"
        print s

def filecmp(x, y):
    """compares filenames with their numeric fields (separated by ".") 
    weighted numerically, i.e.
    "netcount.log.2.gz" < "netcount.log.12.gz" """
    for xs, ys in zip(x.split("."), y.split(".")):
        try:
            res = cmp(int(xs), int(ys)) # try numeric compare
        except ValueError:
            res = cmp(xs, ys)           # else compare strings
        if res:
            return res                  # compare successful
    return cmp(x, y)
           
def process_logs():
    """
    This is the main function in this program. It reads the log
    files produced by the shell script netcount-upd and outputs
    statistics about them.
    """
    # initialize cumulative counters:    
    currec = StatRecord("")             # values from current log entry
    round = StatRecord("")              # current connection (for rounding)
    pch = StatRecord("")                # per call/hourly summary
    daily = StatRecord("-")             # per day summary
    monthly = StatRecord("=")           # per month  -"-
    total = StatRecord("*")             # total displayed summary
    #
    date_l, olddate_l = DateList(), DateList()
    doprint = 0
    wasprint = 0
    do_round = config_opts["accounting"]["round-up"] == "1"
    # We use a string compare so string comparison can be used in order
    # to get as fast as possible to the date we want.
    begin_string = "%4d-%02d-%02d" % tuple(begin[:3])
    #
    fnlist=glob.glob(config_opts["files"]["logfiles"])
    if not fnlist:
        fatalerr("No log files (%s) found!" % \
                   config_opts["files"]["logfiles"], \
                 "This is ok if you're running an new installation", \
                 "and no connections have been completed yet.", \
                 "Otherwise, this means there's something wrong", \
                 "with your logfiles.",
                 "Aborting...")
    fnlist.sort(filecmp)
    fnlist.reverse()
    if __debug__:
        if "f" in vopt:
            print fnlist
    #
    # process files:
    print "Traffic statistics from %s to %s:" % (begin.ymd_string(),
                                                 end.ymd_string())
    oldstate = "start"
    for fname in fnlist:
        try:                            # open next file
            if fname[-3:] == ".gz":
                f = gzip.open(fname)
            else:
                f = open(fname)
        except IOError, e:
            fatalerr("Can't open logfile - %s" % e)
        if __debug__:
            if "f" in vopt:
                print("input file: %s" % fname)
        flineno = 0
        # 
        # gzip doesn't support iterationg over the file object
        for line in f.readlines():
            flineno += 1
            line = line.rstrip()        # strip trailing "\n"
            # print debugging output if desired
            if __debug__:
                if doprint and "r" in vopt: print "> " + line
            if not line: continue       # ignore empty lines
            # parse log entry and check for plausibility
            if line.find("Error") >= 0:
                print line          # pass netcount-upd error msg through
                oldstate = "start"
                continue
            try:
                date, time, dmy, ssec, \
                         dmy, srxb, dmy, stxb, mode = line.split()
            except Exception, e:
                err.inc(line,
                        "Log file %s line %d - Error: %s" % \
                        (fname, flineno, e), \
                        "Parse error. Line ignored")
                oldstate = "start"
                continue
            ## Update state
            # mode contains
            # - "up" if netcount-upd was called during ip-up
            # - "down"            -"-           during ip-down
            # - "snap"            -"-           from cron
            # A "boot-" is prepended if there was no /var/log/nc-sample, i.e.
            # after boot-up or pppd restart
            if mode.startswith("boot"):
                mode = mode[5:]         # strip leading "boot-"
                if mode == "up":        # first ip-up after boot
                    oldstate = "down"
                else:                   # "down" or "snap"                
                    continue            # resynchronize (shouldn't happen)
            # oldstate contains "start" if at first logfile entry,
            # meaning real old state is unknown. Try to synchronize:
            if oldstate == "start":
                if mode == "up":        # record caused by ip-up event
                    oldstate = "down"
                    state = "up"
                elif mode == "down":    # ip-down event. Must have been up.
                    oldstate = "up"
                    state = "down"
                # First entry is of mode "snap". We have to guess from
                # the other values if the net is up.  If the traffic
                # was > 2k, then we can safely assume the net is up and
                # running. Else it may have been some modem or link
                # negotiation and we have to re-check.
                elif (int(stxb) + int(srxb)) > 2000:
                    state = "up"        # net is running
                    oldstate = "up"     # .. and it did so before
                else: continue          # can't guess. Try next record
            elif oldstate in ("up", "down"):
                if mode != "snap":      # if snap: keep current mode
                    state = mode        # else actualize state
            if doprint:
                if __debug__:
                    if "s" in vopt:
                        print "> %s    mode: %4s oldstate: %4s -> state: %-4s"\
                              % (date_l.ymd_string(), mode, oldstate, state)
                try:
                    date_l.string_set(date, time)
                except Exception, e:
                    err.inc(line,
                            "Log file %s line %d - Error: %s" % \
                            (fname, flineno, e), \
                            "Parse error. Line ignored")
                    oldstate = "start"
                    continue
                if date_l < olddate_l:
                    err.inc(line,
                            "^ Log file %s line %d - Error: date error" % \
                            (fname, flineno), \
                            "^ Date is older than previous (%s <> %s)." %
                            (date_l.ymdhms_string(),
                             olddate_l.ymdhms_string()))
                    oldstate = "start"
                    continue
                # See what has changed
                conn_started = state == "up" and oldstate == "down"
                conn_finished = state == "down" and oldstate == "up" 
                hour_changed = date_l[:4] != olddate_l[:4]
                day_changed =  date_l[:3] != olddate_l[:3]
                month_changed = date_l[:3] > monthly.start_date[:3]
                if __debug__:
                    if "c" in vopt:     # display change flags
                        print "(%s) " % olddate_l.ymdhms_string(), \
                              date_l.ymdhms_string(), " ",\
                              ".S"[conn_started] + ".F"[conn_finished], \
                              ".H"[hour_changed] + \
                              ".D"[day_changed] + ".M"[month_changed], \
                              "%-4s" % state                        
                # update counters:
                if oldstate == "up":    # if connection existed
                    sec, rxb, txb = int(ssec), long(srxb), long(stxb)
                    currec.set(olddate_l, sec, rxb, txb)
                    if do_round:
                        round.addrec(currec) # data of current connection
                        if conn_finished:
                            round_val = 60 - round.len % 60
                            currec.len += round_val
                            round.reset(date_l)
                    pch.addrec(currec)  # update accumulated counts
                    daily.addrec(currec)
                    monthly.addrec(currec)
                    total.addrec(currec)
                if intersect("ch", ropt):
                    if conn_finished and "c" in ropt or \
                           hour_changed and "h" in ropt or \
                           day_changed and intersect("dD", ropt) or \
                           month_changed and intersect("mM", ropt):
                        if pch.has_data:
                            pch.prt()
                        pch.reset(date_l)
                if conn_started and "c" in ropt:
                    pch.reset(date_l)   # save starting time
                if day_changed:
                    if "d" in ropt:
                        daily.prt()
                    if "D" in ropt:
                        daily.prt_percent()
                    daily.reset(date_l)
                    # print per month summaries
                    if month_changed:
                        if "m" in ropt:
                            monthly.prt()
                        if "M" in ropt:
                            monthly.prt_percent()
                        monthly.reset(date_l.next_deadline())
                olddate_l[:] = date_l
            # end(if doprint:)
            # check if output should be enabled or disabled
            if date >= begin_string:    # string cmp (for speed)
                doprint = date_l[:3] <= end[:3]
            if doprint and not wasprint:
                date_l.string_set(date, time)
                currec.prt_header()  # print header line
                pch.reset(date_l)
                daily.reset(date_l)
                monthly.reset(date_l.next_deadline())
                total.reset(date_l)
                olddate_l[:] = date_l                
                wasprint = 1
            # remember old values
            oldstate = state
        # end: while line
        f.close()
    # end: for fname in fnlist
    # now dump any records that are not finished
    if wasprint:
        if pch.has_data and intersect("ch", ropt):
            pch.prt()
        if daily.has_data:
            if "d" in ropt:
                daily.prt()
            if "D" in ropt:
                daily.prt_percent()
        if monthly.has_data:
            if "m" in ropt:
                monthly.prt()
            if "M" in ropt:
                monthly.prt_percent()
        if "t" in ropt and total.has_data:
            total.prt("Total:")
    else:
        print "no matching records found"
# end: def process_logs()

cfg = ConfigObject()
begin = DateList()
def go():
    global begin
    #  The following comment is (hopefully) outdated.  It seems to have been
    #  a problem with Python's 2.3 alpha:
    # The following doesn't reset the locales, but should do
    # so. time.strptime() keeps complaining about an unknown locale if
    # LC_CTYPE == de_DE@euro (which shouldn't be unknown anyway and is
    # known everywhere else) even after the call to setlocale(). Thus,
    # this program must be called from a wrapper which sets LC_ALL to
    # "C" for it to work with all legal locale values.
#    locale.setlocale(locale.LC_ALL, "C") # without effect

    cfg.read_config()

    cmdargs()                           # get commandline arguments
    if wopt:
        cfg.write_config(os.path.expanduser(wopt))
    else:
        check_limits()
        # now do the work
        process_logs()

if __name__ == "__main__":
    go()
#
#
# Phew... finished...!
#
#
# Local Variables: ***
# gud-pdb-command-name: "pdb netcount.py" ***
# hcz-A-S-f9: "(pdb \"pdb netcount.py -N \~/netcount-test-rc\")" ***
# hcz-A-f9: "(pdb \"pdb netcount.py\")" ***
# End: ***
