#!/usr/bin/env python
#
# Copyright (C) 2007, Pascal Scheffers <pascal@scheffers.net> 
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# log-collect for OLPC
#
# Compile a report containing:
#  * Basic system information:
#  ** Serial number
#  ** Battery type
#  ** Build number
#  ** Uptime
#  ** disk free space
#  ** ...
#  * Installed packages list
#  * All relevant log files (all of them, at first)
# 
# The report is output as a tarfile
#
# This file has two modes:
# 1. It is a stand-alone python script, when invoked as 'log-collect'
# 2. It is a python module.

import os
import zipfile
import glob
import sys
import time

# The next couple are used by LogSend
import httplib
import mimetypes
import urlparse

class MachineProperties:
    """Various machine properties in easy to access chunks.
    """

    def __read_file(self, filename):
        """Read the entire contents of a file and return it as a string"""

        data = ''

        f = open(filename)
        try:
            data = f.read()
        finally:
            f.close()

        return data

    def olpc_build(self):
        """Buildnumber, from /etc/issue"""
        # Is there a better place to get the build number?
        if not os.path.exists('/etc/issue'):
            return '#/etc/issue not found'

        # Needed, because we want to default to the first non blank line:
        first_line = '' 

        for line in self.__read_file('/etc/issue').splitlines():
            if line.lower().find('olpc build') > -1:
                return line                
            if first_line == '':
                first_line=line

        return first_line

    def uptime(self):
        for line in self.__read_file('/proc/uptime').splitlines():
            if line != '':
                return line            
        return ''

    def loadavg(self):
        for line in self.__read_file('/proc/loadavg').splitlines():
            if line != '':
                return line            
        return ''

    def kernel_version(self):
        for line in self.__read_file('/proc/version').splitlines():
            if line != '':
                return line            
        return ''

    def memfree(self):
        line = ''

        for line in self.__read_file('/proc/meminfo').splitlines():
            if line.find('MemFree:') > -1:
                return line[8:].strip()

    def _mfg_data(self, item):
        """Return mfg data item from /ofw/mfg-data/"""
        
        if not os.path.exists('/ofw/mfg-data/'+item):
            return ''
        
        v = self.__read_file('/ofw/mfg-data/'+item)
        # Remove trailing 0 character, if any:
        if v != '' and ord(v[len(v)-1]) == 0:
            v = v[:len(v)-1]
        
        return v
            
    def laptop_serial_number(self):
        return self._mfg_data('SN')

    def laptop_motherboard_number(self):
        return self._mfg_data('B#')

    def laptop_board_revision(self):
        s = self._mfg_data('SG')[0:1]
        if s == '':
            return ''
            
        return '%02X' % ord(self._mfg_data('SG')[0:1])
        

    def laptop_uuid(self):
        return self._mfg_data('U#')

    def laptop_keyboard(self):
        kb = self._mfg_data('KM') + '-'
        kb += self._mfg_data('KL') + '-'
        kb += self._mfg_data('KV')
        return kb

    def laptop_wireless_mac(self):
        return self._mfg_data('WM')

    def laptop_bios_version(self):
        return self._mfg_data('BV')

    def laptop_country(self):
        return self._mfg_data('LA')

    def laptop_localization(self):
        return self._mfg_data('LO')
    
    def _battery_info(self, item):
        """ from  /sys/class/power-supply/olpc-battery/ """
        root = '/sys/class/power_supply/olpc-battery/'
        if not os.path.exists(root+item):
            return ''
        
        return self.__read_file(root+item).strip()

    def battery_serial_number(self):
        return self._battery_info('serial_number')
    
    def battery_capacity(self):
        return self._battery_info('capacity') + ' ' + \
                    self._battery_info('capacity_level')

    def battery_info(self):
        #Should be just:
        #return self._battery_info('uevent')
        
        #But because of a bug in the kernel, that has trash, lets filter:
        bi = ''        
        for line in self._battery_info('uevent').splitlines():
            if line.startswith('POWER_'):
                bi += line + '\n'
        
        return bi
       
    def disksize(self, path):
        return os.statvfs(path).f_bsize * os.statvfs(path).f_blocks
    
    def diskfree(self, path):
        return os.statvfs(path).f_bsize * os.statvfs(path).f_bavail
        
    def _read_popen(self, cmd):
        p = os.popen(cmd)
        s = ''
        try:
            for line in p:
                s += line 
        finally:
            p.close()        
        
        return s
    
    def ifconfig(self):        
        return self._read_popen('/sbin/ifconfig')
               
    def route_n(self):        
        return self._read_popen('/sbin/route -n')
    
    def df_a(self):
        return self._read_popen('/bin/df -a')
  
    def ps_auxfwww(self):
        return self._read_popen('/bin/ps auxfwww')
    
    def usr_bin_free(self):
        return self._read_popen('/usr/bin/free')

    def top(self):
        return self._read_popen('/usr/bin/top -bn2')
        
    def installed_activities(self):        
        s = ''        
        for path in glob.glob('/usr/share/activities/*.activity'):
            s += os.path.basename(path) + '\n'

        for path in glob.glob('/home/olpc/Activities/*'):
            s += '~' + os.path.basename(path) + '\n'
            
        return s
        
        

class LogCollect:
    """Collect XO logfiles and machine metadata for reporting to OLPC

    """
    def __init__(self):
        self._mp = MachineProperties()

    def write_logs(self, archive='', logbytes=15360):
        """Write a zipfile containing the tails of the logfiles and machine info of the XO
        
        Arguments:
            archive -   Specifies the location where to store the data
                        defaults to /dev/shm/logs-<xo-serial>.zip
                        
            logbytes -  Maximum number of bytes to read from each log file.
                        0 means complete logfiles, not just the tail
                        -1 means only save machine info, no logs
        """
        #This function is crammed with try...except to make sure we get as much
        #data as possible, if anything fails.
        
        if archive=='':
            archive = '/dev/shm/logs.zip'
            try:
                #With serial number is more convenient, but might fail for some
                #Unknown reason...
                archive = '/dev/shm/logs-%s.zip' % self._mp.laptop_serial_number()
            except Exception:
                pass
            
        z = zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED)
        
        try:            
            try: 
                z.writestr('info.txt', self.laptop_info())
            except Exception, e:
                z.writestr('info.txt',
                           "logcollect: could not add info.txt: %s" % e)
            
            if logbytes > -1:            
                # Include some log files from /var/log.
                for fn in ['dmesg', 'messages', 'cron', 'maillog','rpmpkgs',
                           'Xorg.0.log', 'spooler']:
                    try:
                        if os.access('/var/log/'+fn, os.F_OK):
                            if logbytes == 0:
                                z.write('/var/log/'+fn, 'var-log/'+fn)
                            else:
                                z.writestr('var-log/'+fn,
                                           self.file_tail('/var/log/'+fn, logbytes))
                    except Exception, e:
                        z.writestr('var-log/'+fn,
                                   "logcollect: could not add %s: %s" % (fn, e))
                        
                # Include all current ones from sugar/logs
                for path in glob.glob('/home/olpc/.sugar/default/logs/*.log'):
                    try:
                        if os.access(path, os.F_OK):
                            if logbytes == 0:
                                z.write(path, 'sugar-logs/'+os.path.basename(path))
                            else:
                                z.writestr('sugar-logs/'+os.path.basename(path),
                                           self.file_tail(path, logbytes))
                    except Exception, e:
                        z.writestr('sugar-logs/'+fn,
                                   "logcollect: could not add %s: %s" % (fn, e))
                try:                 
                    z.write('/etc/resolv.conf')
                except Exception, e:
                    z.writestr('/etc/resolv.conf',
                               "logcollect: could not add resolv.conf: %s" % e)
                    
        except Exception, e:
            print 'While creating zip archive: %s' % e            
        
        z.close()        
        
        return archive

    def file_tail(self, filename, tailbytes):
        """Read the tail (end) of the file
        
        Arguments:
            filename    The name of the file to read
            tailbytes   Number of bytes to include or 0 for entire file
        """

        data = ''

        f = open(filename)
        try:
            fsize = os.stat(filename).st_size
            
            if tailbytes > 0 and fsize > tailbytes:
                f.seek(-tailbytes, 2)
                
            data = f.read()
        finally:
            f.close()

        return data        
              

    def make_report(self, target='stdout'):
        """Create the report

        Arguments:
            target - where to save the logs, a path or stdout            

        """

        li = self.laptop_info()
        for k, v in li.iteritems():
            print k + ': ' +v
            
        print self._mp.battery_info()

    def laptop_info(self):
        """Return a string with laptop serial, battery type, build, memory info, etc."""

        s = ''        
        try:
            # Do not include UUID!
            s += 'laptop-info-version: 1.0\n'
            s += 'clock: %f\n' % time.clock()
            s += 'date: %s' % time.strftime("%a, %d %b %Y %H:%M:%S +0000",
                                            time.gmtime())
            s += 'memfree: %s\n' % self._mp.memfree()
            s += 'disksize: %s MB\n' % ( self._mp.disksize('/') / (1024*1024) ) 
            s += 'diskfree: %s MB\n' % ( self._mp.diskfree('/') / (1024*1024) ) 
            s += 'olpc_build: %s\n' % self._mp.olpc_build()
            s += 'kernel_version: %s\n' % self._mp.kernel_version()
            s += 'uptime: %s\n' % self._mp.uptime()
            s += 'loadavg: %s\n' % self._mp.loadavg()        
            s += 'serial-number: %s\n' % self._mp.laptop_serial_number()
            s += 'motherboard-number: %s\n' % self._mp.laptop_motherboard_number()
            s += 'board-revision: %s\n' %  self._mp.laptop_board_revision()
            s += 'keyboard: %s\n' %  self._mp.laptop_keyboard()
            s += 'wireless_mac: %s\n' %  self._mp.laptop_wireless_mac()
            s += 'firmware: %s\n' %  self._mp.laptop_bios_version()
            s += 'country: %s\n' % self._mp.laptop_country()
            s += 'localization: %s\n' % self._mp.laptop_localization()
                
            s += self._mp.battery_info()
            
            s += "\n[/sbin/ifconfig]\n%s\n" % self._mp.ifconfig()
            s += "\n[/sbin/route -n]\n%s\n" % self._mp.route_n()
            
            s += '\n[Installed Activities]\n%s\n' % self._mp.installed_activities()
    
            s += '\n[df -a]\n%s\n' % self._mp.df_a()
            s += '\n[ps auxwww]\n%s\n' % self._mp.ps_auxfwww()
            s += '\n[free]\n%s\n' % self._mp.usr_bin_free()
            s += '\n[top -bn2]\n%s\n' % self._mp.top()
        except Exception, e:
            s += '\nException while building info:\n%s\n' % e
        
        return s

class LogSend:
    
    # post_multipart and encode_multipart_formdata have been taken from
    #  http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
    def post_multipart(self, host, selector, fields, files):
        """
        Post fields and files to an http host as multipart/form-data.
        fields is a sequence of (name, value) elements for regular form fields.
        files is a sequence of (name, filename, value) elements for data to be uploaded as files
        Return the server's response page.        
        """
        content_type, body = self.encode_multipart_formdata(fields, files)
        h = httplib.HTTP(host)
        h.putrequest('POST', selector)
        h.putheader('content-type', content_type)
        h.putheader('content-length', str(len(body)))
        h.putheader('Host', host)
        h.endheaders()
        h.send(body)
        errcode, errmsg, headers = h.getreply()
        return h.file.read()
    
    def encode_multipart_formdata(self, fields, files):
        """
        fields is a sequence of (name, value) elements for regular form fields.
        files is a sequence of (name, filename, value) elements for data to be uploaded as files
        Return (content_type, body) ready for httplib.HTTP instance
        """
        BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
        CRLF = '\r\n'
        L = []
        for (key, value) in fields:
            L.append('--' + BOUNDARY)
            L.append('Content-Disposition: form-data; name="%s"' % key)
            L.append('')
            L.append(value)
        for (key, filename, value) in files:
            L.append('--' + BOUNDARY)
            L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
            L.append('Content-Type: %s' % self.get_content_type(filename))
            L.append('')
            L.append(value)
        L.append('--' + BOUNDARY + '--')
        L.append('')
        body = CRLF.join(L)
        content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
        return content_type, body
    
    def read_file(self, filename):
        """Read the entire contents of a file and return it as a string"""

        data = ''

        f = open(filename)
        try:
            data = f.read()
        finally:
            f.close()

        return data

    def get_content_type(self, filename):
        return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
        
    def http_post_logs(self, url, archive):
        #host, selector, fields, files
        files = ('logs', os.path.basename(archive), self.read_file(archive)),
        
        # Client= olpc will make the server return just "OK" or "FAIL"
        fields = ('client', 'xo'),
        urlparts = urlparse.urlsplit(url)
        print "Sending logs to %s" % url
        r = self.post_multipart(urlparts[1], urlparts[2], fields, files)
        print r
        return (r == 'OK')


# This script is dual-mode, it can be used as a command line tool and as
# a library. 
if sys.argv[0].endswith('logcollect.py') or \
        sys.argv[0].endswith('logcollect'):
    print 'log-collect utility 1.0'
        
    lc = LogCollect()
    ls = LogSend()

    logs = ''
    mode = 'http'
    
    if len(sys.argv)==1:
        print """logcollect.py - send your XO logs to OLPC
        
Usage:
    logcollect.py http  - send logs to default server
    
    logcollect.py http://server.name/submit.php
                         - submit logs to alternative server
                         
    logcollect.py file:/media/xxxx-yyyy/mylog.zip
                         - save the zip file on a USB device or SD card
                         
    logcollect.py all file:/media/xxxx-yyyy/mylog.zip
                        - Save to zip file and include ALL logs

    logcollect.py none http
                        - Just send info.txt, but no logs via http.

    logcollect.py none file
                        - Just save info.txt in /dev/shm/logs-SN123.zip

    If you specify 'all' or 'none' you must specify http or file as well.
        """
        sys.exit()
        
    
    logbytes = 15360   
    if len(sys.argv)>1:
        mode = sys.argv[len(sys.argv)-1]
        if sys.argv[1] == 'all':
            logbytes = 0
        if sys.argv[1] == 'none':
            logbytes = -1
   

    if mode.startswith('file'):
        # file://
        logs = mode[5:]
 
    #if mode.lower().startswith('http'):
    #    pass
    #else if mode.lower().startswith('usb'):
    #    pass
    #else if mode.lower().startswith('sd'):
    #    pass
    
    logs = lc.write_logs(logs, logbytes)
    print 'Logs saved in %s' % logs
    
    sent_ok = False
    if len(sys.argv)>1:
       mode = sys.argv[len(sys.argv)-1]
    
    if mode.startswith('http'):
        print "Trying to send the logs using HTTP (web)"
        if len(mode) == 4:
            url = 'http://olpc.scheffers.net/olpc/submit.tcl'
        else:
            url = mode
            
        if ls.http_post_logs(url, logs):
            print "Logs were sent."
            sent_ok = True            
        else:
            print "FAILED to send logs."
 

    if sent_ok:
        os.remove(logs)
        print "Logs were sent, tempfile deleted."


