Ticket #1439: 0001-New-control-panel-Report-a-problem.patch

File 0001-New-control-panel-Report-a-problem.patch, 38.6 KB (added by wadeb, 15 years ago)

Initial patch

  • configure.ac

    From ac617f8585b4ef457e5860aadfb162c563deef3a Mon Sep 17 00:00:00 2001
    From: Wade Brainerd <wadetb@gmail.com>
    Date: Fri, 16 Oct 2009 22:20:06 -0400
    Subject: [PATCH] New control panel: "Report a problem"
    
    Gathers system and Sugar logs, system information, and user notes.  Zips them all up and uploads them to a central server.
    
    See http://wiki.sugarlabs.org/go/Features/Problem_Reports
    ---
     configure.ac                              |    1 +
     data/icons/Makefile.am                    |    3 +-
     data/icons/module-report.svg              |  140 ++++++++
     data/sugar.schemas.in                     |   12 +
     extensions/cpsection/Makefile.am          |    2 +-
     extensions/cpsection/report/Makefile.am   |    7 +
     extensions/cpsection/report/__init__.py   |   23 ++
     extensions/cpsection/report/logcollect.py |  506 +++++++++++++++++++++++++++++
     extensions/cpsection/report/model.py      |   72 ++++
     extensions/cpsection/report/view.py       |   95 ++++++
     10 files changed, 859 insertions(+), 2 deletions(-)
     create mode 100644 data/icons/module-report.svg
     create mode 100644 extensions/cpsection/report/Makefile.am
     create mode 100644 extensions/cpsection/report/__init__.py
     create mode 100644 extensions/cpsection/report/logcollect.py
     create mode 100644 extensions/cpsection/report/model.py
     create mode 100644 extensions/cpsection/report/view.py
    
    diff --git a/configure.ac b/configure.ac
    index fe3e6eb..11e398c 100644
    a b extensions/cpsection/network/Makefile 
    5959extensions/cpsection/power/Makefile
    6060extensions/cpsection/updater/backends/Makefile
    6161extensions/cpsection/updater/Makefile
     62extensions/cpsection/report/Makefile
    6263extensions/deviceicon/Makefile
    6364extensions/globalkey/Makefile
    6465extensions/Makefile
  • data/icons/Makefile.am

    diff --git a/data/icons/Makefile.am b/data/icons/Makefile.am
    index d2f4ede..8e2f14a 100644
    a b sugar_DATA = \ 
    99        module-language.svg             \
    1010        module-network.svg              \
    1111        module-power.svg                \
    12         module-updater.svg
     12        module-updater.svg              \
     13        module-report.svg
    1314
    1415EXTRA_DIST = $(sugar_DATA)
  • new file data/icons/module-report.svg

    diff --git a/data/icons/module-report.svg b/data/icons/module-report.svg
    new file mode 100644
    index 0000000..b6eb479
    - +  
     1<?xml version="1.0" ?><!DOCTYPE svg  PUBLIC '-//W3C//DTD SVG 1.1//EN'  'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
     2        <!ENTITY stroke_color "#000">
     3        <!ENTITY fill_color "#fff">
     4]>
     5<!-- Created with Inkscape (http://www.inkscape.org/) -->
     6
     7<svg
     8   xmlns:svg="http://www.w3.org/2000/svg"
     9   xmlns="http://www.w3.org/2000/svg"
     10   version="1.0"
     11   width="55"
     12   height="55"
     13   id="svg2">
     14  <defs
     15     id="defs4" />
     16  <g
     17     transform="translate(7.5,4)"
     18     id="g111">
     19    <g
     20       id="g113">
     21      <g
     22         transform="matrix(1.7665356,0,0,1.7665356,-26.206699,-23.703877)"
     23         id="g3188"
     24         style="stroke:&fill_color;">
     25        <path
     26           d="m 27.377856,20.343519 c 0.962106,-1.112699 2.665207,-2.669397 4.202937,-1.727197 1.292449,0.502803 2.775585,1.6621 1.957512,3.201625 -0.182463,0.554447 -0.432963,1.088431 -0.549694,1.663075"
     27           id="path3252"
     28           style="fill:none;stroke:&fill_color;;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
     29        <path
     30           d="m 31.312226,33.70644 c -1.808083,3.055021 -4.510034,5.979882 -8.102466,6.775304 -2.013184,0.487828 -4.263446,-0.569022 -5.04412,-2.515946 -1.345509,-3.223928 -0.393273,-6.90514 1.067536,-9.918324 1.73681,-3.382313 4.485564,-6.630023 8.268217,-7.697555 1.971076,-0.597449 4.330009,0.204289 5.255634,2.110869 1.485139,2.991757 0.708574,6.557327 -0.549181,9.486277 -0.266768,0.601928 -0.566447,1.189201 -0.89562,1.759375 z"
     31           id="path3197"
     32           style="fill:none;stroke:&fill_color;;stroke-width:1.51091182;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     33        <path
     34           d="m 30.640788,20.96543 c -1.100558,1.983085 -2.202454,3.965429 -3.301174,5.949533"
     35           id="path3199"
     36           style="fill:none;stroke:&fill_color;;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
     37        <path
     38           d="m 23.640267,22.485322 c 0.887247,1.787445 2.309517,3.236439 3.831245,4.497123 1.895179,0.603607 3.902778,1.265885 5.914011,1.031489"
     39           id="path3201"
     40           style="fill:none;stroke:&fill_color;;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" />
     41        <path
     42           d="m 22.8141,25.484531 c 0.801557,0.826125 1.598557,1.656779 2.406938,2.476234"
     43           id="path3203"
     44           style="fill:none;stroke:&fill_color;;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     45        <path
     46           d="m 21.785315,26.831706 c 0.8524,0.8851 1.711393,1.7641 2.551072,2.661318"
     47           id="path3207"
     48           style="fill:none;stroke:&fill_color;;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     49        <path
     50           d="m 20.834693,28.29192 c 0.874972,0.908554 1.756543,1.811038 2.617044,2.733363"
     51           id="path3209"
     52           style="fill:none;stroke:&fill_color;;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     53        <path
     54           d="m 20.016015,29.896225 c 0.852399,0.8851 1.711392,1.7641 2.551071,2.661318"
     55           id="path3211"
     56           style="fill:none;stroke:&fill_color;;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     57        <path
     58           d="m 19.370877,31.63468 c 0.77331,0.815057 1.576967,1.605019 2.311559,2.455123"
     59           id="path3213"
     60           style="fill:none;stroke:&fill_color;;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     61        <path
     62           d="m 19.111902,33.697954 c 0.513893,0.585955 1.350409,1.527516 1.685883,1.92411"
     63           id="path3215"
     64           style="fill:none;stroke:&fill_color;;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     65        <path
     66           d="m 19.133652,36.330772 c 0.267093,0.267429 0.533177,0.536576 0.779483,0.823549"
     67           id="path3217"
     68           style="fill:none;stroke:&fill_color;;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     69        <path
     70           d="M 30.786085,30.087159 C 29.669861,29.806053 28.551993,29.531158 27.438134,29.240807"
     71           id="path3230"
     72           style="fill:none;stroke:&fill_color;;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     73        <path
     74           d="M 30.133789,31.6517 C 28.94107,31.35605 27.750337,31.05164 26.553484,30.773066"
     75           id="path3232"
     76           style="fill:none;stroke:&fill_color;;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     77        <path
     78           d="M 29.344518,33.20507 C 28.120201,32.9016 26.897842,32.589379 25.668834,32.305325"
     79           id="path3234"
     80           style="fill:none;stroke:&fill_color;;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     81        <path
     82           d="m 28.364488,34.716219 c -1.192719,-0.29565 -2.383452,-0.60006 -3.580305,-0.878634"
     83           id="path3236"
     84           style="fill:none;stroke:&fill_color;;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     85        <path
     86           d="M 27.18151,36.144152 C 26.088995,35.881974 25.00304,35.580967 23.899532,35.369845"
     87           id="path3238"
     88           style="fill:none;stroke:&fill_color;;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     89        <path
     90           d="m 25.524151,37.400068 c -0.764399,-0.152067 -1.998073,-0.40573 -2.50927,-0.497963"
     91           id="path3240"
     92           style="fill:none;stroke:&fill_color;;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     93        <path
     94           d="m 23.233189,38.697641 c -0.365147,-0.09759 -0.731278,-0.193457 -1.102957,-0.263278"
     95           id="path3242"
     96           style="fill:none;stroke:&fill_color;;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     97        <path
     98           d="m 38.499043,20.363658 c -1.017952,1.881865 -4.079717,1.462776 -4.758813,-0.494479 -0.756214,-1.610078 0.700774,-3.591894 2.455951,-3.402434 1.878969,0.03791 3.303331,2.217955 2.302862,3.896913 z"
     99           id="path3244"
     100           style="fill:none;stroke:&fill_color;;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     101        <path
     102           d="m 36.397626,17.843099 c -0.91168,1.292999 -2.210038,-1.183985 -0.578475,-1.094073 0.497596,0.05601 0.846524,0.647517 0.578475,1.094073 z"
     103           id="path3246"
     104           style="fill:none;stroke:&fill_color;;stroke-width:0.79813904;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     105        <path
     106           d="m 32.748117,16.754734 c -1.017951,1.881865 -4.079717,1.462776 -4.758813,-0.494479 -0.756213,-1.610078 0.700775,-3.591894 2.455952,-3.402434 1.871021,0.02637 3.307365,2.231278 2.302861,3.896913 z"
     107           id="path3248"
     108           style="fill:none;stroke:&fill_color;;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     109        <path
     110           d="m 30.781161,14.311805 c -0.911681,1.292999 -2.210039,-1.183985 -0.578475,-1.094073 0.497596,0.05601 0.846524,0.647517 0.578475,1.094073 z"
     111           id="path3250"
     112           style="fill:none;stroke:&fill_color;;stroke-width:0.79813904;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
     113        <path
     114           d="M 26.988213,20.231746 C 26.282875,18.987831 25.577538,17.743915 24.8722,16.5 c -1.603332,0.1 -3.206664,0.2 -4.809996,0.3"
     115           id="path3260"
     116           style="fill:none;stroke:&fill_color;;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
     117        <path
     118           d="m 33.361036,23.684723 c 1.528205,0.02338 3.056409,0.04676 4.584614,0.07014 0.62756,1.23389 1.255121,2.46778 1.882681,3.70167"
     119           id="path3262"
     120           style="fill:none;stroke:&fill_color;;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
     121        <path
     122           d="m 23.851158,22.042526 c -0.365874,-1.022406 -0.731748,-2.044811 -1.097622,-3.067217 -2.721645,0.918756 -5.443289,1.837512 -8.164934,2.756268"
     123           id="path3264"
     124           style="fill:none;stroke:&fill_color;;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
     125        <path
     126           d="m 33.777334,27.207476 c 1.158007,-0.142593 2.316013,-0.285187 3.47402,-0.42778 0.165967,2.472733 0.331935,4.945467 0.497902,7.4182"
     127           id="path3266"
     128           style="fill:none;stroke:&fill_color;;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
     129        <path
     130           d="m 20.388485,25.668558 c -0.346432,-0.886626 -0.692865,-1.773252 -1.039297,-2.659878 -2.288056,2.168901 -4.576111,4.337803 -6.864167,6.506704"
     131           id="path3268"
     132           style="fill:none;stroke:&fill_color;;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
     133        <path
     134           d="m 32.410841,31.851473 c 0.973352,-0.305797 1.946705,-0.611593 2.920057,-0.91739 -0.675933,3.13739 -1.351865,6.274779 -2.027798,9.412169"
     135           id="path3270"
     136           style="fill:none;stroke:&fill_color;;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
     137      </g>
     138    </g>
     139  </g>
     140</svg>
  • data/sugar.schemas.in

    diff --git a/data/sugar.schemas.in b/data/sugar.schemas.in
    index 8c43930..831e011 100644
    a b  
    134134    </schema>
    135135
    136136    <schema>
     137      <key>/schemas/desktop/sugar/logcollect_server</key>
     138      <applyto>/desktop/sugar/logcollect_server</applyto>
     139      <owner>sugar</owner>
     140      <type>string</type>
     141      <default>logcollect.sugarlabs.org</default>
     142      <locale name="C">
     143        <short>Log Collect Server</short>
     144        <long>Url of the server logs are uploaded to.</long>
     145      </locale>
     146    </schema>
     147
     148    <schema>
    137149      <key>/schemas/desktop/sugar/power/automatic</key>
    138150      <applyto>/desktop/sugar/power/automatic</applyto>
    139151      <owner>sugar</owner>
  • extensions/cpsection/Makefile.am

    diff --git a/extensions/cpsection/Makefile.am b/extensions/cpsection/Makefile.am
    index dd0a6b8..f20968e 100644
    a b  
    1 SUBDIRS = aboutme aboutcomputer datetime frame keyboard language network power updater
     1SUBDIRS = aboutme aboutcomputer datetime frame keyboard language network power updater report
    22
    33sugardir = $(pkgdatadir)/extensions/cpsection
    44sugar_PYTHON = __init__.py
  • new file extensions/cpsection/report/Makefile.am

    diff --git a/extensions/cpsection/report/Makefile.am b/extensions/cpsection/report/Makefile.am
    new file mode 100644
    index 0000000..5c5a31a
    - +  
     1sugardir = $(pkgdatadir)/extensions/cpsection/report
     2
     3sugar_PYTHON =          \
     4        __init__.py     \
     5        logcollect.py   \
     6        model.py        \
     7        view.py         
  • new file extensions/cpsection/report/__init__.py

    diff --git a/extensions/cpsection/report/__init__.py b/extensions/cpsection/report/__init__.py
    new file mode 100644
    index 0000000..24082f7
    - +  
     1# Copyright (C) 2008, OLPC
     2#
     3# This program is free software; you can redistribute it and/or modify
     4# it under the terms of the GNU General Public License as published by
     5# the Free Software Foundation; either version 2 of the License, or
     6# (at your option) any later version.
     7#
     8# This program is distributed in the hope that it will be useful,
     9# but WITHOUT ANY WARRANTY; without even the implied warranty of
     10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     11# GNU General Public License for more details.
     12#
     13# You should have received a copy of the GNU General Public License
     14# along with this program; if not, write to the Free Software
     15# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
     16
     17from gettext import gettext as _
     18
     19CLASS = 'ProblemReport'
     20ICON = 'module-report'
     21TITLE = _('Report a problem')
     22
     23
  • new file extensions/cpsection/report/logcollect.py

    diff --git a/extensions/cpsection/report/logcollect.py b/extensions/cpsection/report/logcollect.py
    new file mode 100644
    index 0000000..5820571
    - +  
     1# Copyright (C) 2007, Pascal Scheffers <pascal@scheffers.net>
     2#
     3# Permission is hereby granted, free of charge, to any person
     4# obtaining a copy of this software and associated documentation
     5# files (the "Software"), to deal in the Software without
     6# restriction, including without limitation the rights to use,
     7# copy, modify, merge, publish, distribute, sublicense, and/or sell
     8# copies of the Software, and to permit persons to whom the
     9# Software is furnished to do so, subject to the following
     10# conditions:
     11#
     12# The above copyright notice and this permission notice shall be
     13# included in all copies or substantial portions of the Software.
     14#
     15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
     16# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
     17# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
     18# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
     19# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
     20# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
     21# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
     22# OTHER DEALINGS IN THE SOFTWARE.
     23#
     24# log-collect for Sugar
     25#
     26# Compile a report containing:
     27#  * Basic system information:
     28#  ** Serial number
     29#  ** Battery type
     30#  ** Build number
     31#  ** Uptime
     32#  ** disk free space
     33#  ** ...
     34#  * Installed packages list
     35#  * All relevant log files (all of them, at first)
     36#
     37# The report is output as a tarfile
     38#
     39# This file has two modes:
     40# 1. It is a stand-alone python script, when invoked as 'log-collect'
     41# 2. It is a python module.
     42
     43import os
     44import zipfile
     45import glob
     46import sys
     47import time
     48import subprocess
     49import logging
     50
     51# The next couple are used by LogSend
     52import httplib
     53import mimetypes
     54import urlparse
     55
     56# Sugar info
     57import jarabe.config
     58import sugar.presence.presenceservice
     59
     60_logger = logging.getLogger('LogCollect')
     61
     62class MachineProperties:
     63    """Various machine properties in easy to access chunks.
     64    """
     65
     66    def __read_file(self, filename):
     67        """Read the entire contents of a file and return it as a string"""
     68
     69        try:
     70            f = open(filename)
     71            data = f.read()
     72            f.close()
     73            return data
     74       
     75        except Exception, e:
     76            _logger.debug("Unable to read '%s':\n%s\n" % (filename, e))
     77            return ''
     78
     79    def release_id(self):
     80        build_no = self.__read_file('/boot/olpc_build')
     81   
     82        if build_no == '':
     83            build_no = self.__read_file('/etc/redhat-release')
     84   
     85        if build_no == '':
     86            try:
     87                popen = subprocess.Popen(['lsb_release', '-ds'],
     88                                         stdout=subprocess.PIPE)
     89            except OSError, e:
     90                if e.errno != errno.ENOENT:
     91                    raise
     92            else:
     93                build_no, stderr_ = popen.communicate()
     94       
     95        if build_no == '':
     96            build_no = 'Not available'
     97       
     98        return build_no.strip()
     99
     100    def uptime(self):
     101        for line in self.__read_file('/proc/uptime').splitlines():
     102            if line != '':
     103                return line           
     104        return ''
     105
     106    def loadavg(self):
     107        for line in self.__read_file('/proc/loadavg').splitlines():
     108            if line != '':
     109                return line           
     110        return ''
     111
     112    def kernel_version(self):
     113        for line in self.__read_file('/proc/version').splitlines():
     114            if line != '':
     115                return line           
     116        return ''
     117
     118    def memfree(self):
     119        for line in self.__read_file('/proc/meminfo').splitlines():
     120            if line.find('MemFree:') > -1:
     121                return line[8:].strip()
     122
     123    def _ofw_mfg_data(self, item):
     124        """Return mfg data item from /ofw/mfg-data/"""
     125       
     126        if not os.path.exists('/ofw/mfg-data/'+item):
     127            return ''
     128       
     129        v = self.__read_file('/ofw/mfg-data/'+item)
     130        # Remove trailing 0 character, if any:
     131        if v != '' and ord(v[len(v)-1]) == 0:
     132            v = v[:len(v)-1]
     133       
     134        return v
     135           
     136    def ofw_serial_number(self):
     137        return self._ofw_mfg_data('SN')
     138
     139    def ofw_motherboard_number(self):
     140        return self._ofw_mfg_data('B#')
     141
     142    def ofw_board_revision(self):
     143        s = self._ofw_mfg_data('SG')[0:1]
     144        if s == '':
     145            return ''
     146           
     147        return '%02X' % ord(self._ofw_mfg_data('SG')[0:1])
     148
     149    def ofw_uuid(self):
     150        return self._ofw_mfg_data('U#')
     151
     152    def ofw_keyboard(self):
     153        kb = self._ofw_mfg_data('KM') + '-'
     154        kb += self._ofw_mfg_data('KL') + '-'
     155        kb += self._ofw_mfg_data('KV')
     156        return kb
     157
     158    def ofw_wireless_mac(self):
     159        return self._ofw_mfg_data('WM')
     160
     161    def ofw_bios_version(self):
     162        return self._ofw_mfg_data('BV')
     163
     164    def ofw_country(self):
     165        return self._ofw_mfg_data('LA')
     166
     167    def ofw_localization(self):
     168        return self._ofw_mfg_data('LO')
     169   
     170    def _xo_battery_info(self, item):
     171        """ from  /sys/class/power-supply/olpc-battery/ """
     172        root = '/sys/class/power_supply/olpc-battery/'
     173        if not os.path.exists(root+item):
     174            return ''
     175       
     176        return self.__read_file(root+item).strip()
     177
     178    def xo_battery_serial_number(self):
     179        return self._xo_battery_info('serial_number')
     180   
     181    def xo_battery_capacity(self):
     182        return self._xo_battery_info('capacity') + ' ' + \
     183                    self._xo_battery_info('capacity_level')
     184
     185    def xo_battery_info(self):
     186        #Should be just:
     187        #return self._xo_battery_info('uevent')
     188       
     189        #But because of a bug in the kernel, that has trash, lets filter:
     190        bi = ''       
     191        for line in self._xo_battery_info('uevent').splitlines():
     192            if line.startswith('POWER_'):
     193                bi += line + '\n'
     194       
     195        return bi
     196       
     197    def disksize(self, path):
     198        return os.statvfs(path).f_bsize * os.statvfs(path).f_blocks
     199   
     200    def diskfree(self, path):
     201        return os.statvfs(path).f_bsize * os.statvfs(path).f_bavail
     202       
     203    def _read_popen(self, cmd):
     204        p = os.popen(cmd)
     205        s = ''
     206        try:
     207            for line in p:
     208                s += line
     209        finally:
     210            p.close()       
     211       
     212        return s
     213   
     214    def ifconfig(self):       
     215        return self._read_popen('/sbin/ifconfig')
     216               
     217    def route_n(self):       
     218        return self._read_popen('/sbin/route -n')
     219   
     220    def df_a(self):
     221        return self._read_popen('/bin/df -a')
     222 
     223    def ps_auxfwww(self):
     224        return self._read_popen('/bin/ps auxfwww')
     225   
     226    def usr_bin_free(self):
     227        return self._read_popen('/usr/bin/free')
     228
     229    def top(self):
     230        return self._read_popen('/usr/bin/top -bn2')
     231   
     232    def activity_version(self, path):
     233        try:
     234            fd = open(path + '/activity/activity.info', "r")
     235            lines = fd.readlines()
     236            fd.close()
     237           
     238            for line in lines:
     239                if line.startswith("activity_version"):
     240                    return line.split('=')[1].strip()
     241           
     242            return "Unable to find activity version in activity.info."
     243       
     244        except Exception, e:
     245            return "Unable to get activity version for %s:\n%s\n" % (path, e)
     246   
     247    def activity_bundle_id(self, path):
     248        try:
     249            fd = open(path + '/activity/activity.info', "r")
     250            lines = fd.readlines()
     251            fd.close()
     252           
     253            for line in lines:
     254                if line.startswith("bundle_id") or line.startswith('service_name'):
     255                    return line.split('=')[1].strip()
     256           
     257            return "Unable to find activity bundle id in activity.info."
     258       
     259        except Exception, e:
     260            return "Unable to get activity bundle id for %s:\n%s\n" % (path, e)
     261   
     262    def installed_activities(self):
     263        paths = []
     264        paths.extend(glob.glob(os.environ['SUGAR_PATH'] + '/activities/*.activity'))
     265        paths.extend(glob.glob(os.environ['HOME'] + '/Activities/*'))
     266       
     267        s = ''
     268        for path in paths:
     269            s += '\nbundle_id: %s\n' % self.activity_bundle_id(path)
     270            s += 'activity_version: %s\n' % self.activity_version(path)
     271            s += 'path: %s\n' % path
     272       
     273        return s
     274
     275    def sugar_version(self):
     276        return jarabe.config.version
     277
     278    def user_nick(self):
     279        return sugar.presence.presenceservice.get_instance().get_owner().props.nick
     280
     281class LogCollect:
     282    """Collect XO logfiles and machine metadata for reporting to Sugar Labs
     283
     284    """
     285    def __init__(self):
     286        self._mp = MachineProperties()
     287
     288    def write_logs(self, notes, archive='', logbytes=16384):
     289        """Write a zipfile containing the tails of the logfiles and machine info of the XO
     290       
     291        Arguments:
     292            notes -     User notes accompanying the log
     293           
     294            archive -   Specifies the location where to store the data
     295                        defaults to /dev/shm/sugar-report.zip
     296                       
     297            logbytes -  Maximum number of bytes to read from each log file.
     298                        0 means complete logfiles, not just the tail
     299                        -1 means only save machine info, no logs
     300        """
     301        #This function is crammed with try...except to make sure we get as much
     302        #data as possible, if anything fails.
     303       
     304        if archive=='':
     305            archive = '/dev/shm/sugar-report.zip'
     306       
     307        z = zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED)
     308       
     309        try:           
     310            try:
     311                z.writestr('info.txt', self.system_info(notes))
     312            except Exception, e:
     313                z.writestr('info.txt',
     314                           "logcollect: could not add info.txt: %s" % e)
     315
     316            if logbytes > -1:           
     317                # Include some log files from /var/log.
     318                for fn in ['dmesg', 'messages', 'cron', 'maillog','rpmpkgs',
     319                           'Xorg.0.log', 'spooler']:
     320                    self.write_log(z, '/var/log/'+fn, 'var-log/'+fn, logbytes)
     321
     322                # Include all current ones from sugar/logs
     323                for path in glob.glob('%s/.sugar/default/logs/*.log' % os.environ['HOME']):
     324                    self.write_log(z, path, 'sugar-logs/'+os.path.basename(path), 0)
     325
     326                # Any other misc files
     327                self.write_log(z, '/etc/resolv.conf', 'etc/resolv.conf', logbytes)
     328       
     329        except Exception, e:
     330            _logger.debug('While creating zip archive: %s' % e)
     331       
     332        z.close()       
     333       
     334        return archive
     335
     336    def write_log(self, z, realpath, fakepath, logbytes):
     337        try:
     338            if os.access(realpath, os.F_OK):
     339                if logbytes == 0:
     340                    z.write(realpath, fakepath)
     341                else:
     342                    z.writestr(fakepath, self.file_tail(realpath, logbytes))
     343        except Exception, e:
     344            z.writestr(fakepath, "logcollect: could not add %s: %s" % (realpath, e))
     345       
     346    def file_tail(self, filename, tailbytes):
     347        """Read the tail (end) of the file
     348       
     349        Arguments:
     350            filename    The name of the file to read
     351            tailbytes   Number of bytes to include or 0 for entire file
     352        """
     353
     354        data = ''
     355
     356        f = open(filename)
     357        try:
     358            fsize = os.stat(filename).st_size
     359           
     360            if tailbytes > 0 and fsize > tailbytes:
     361                f.seek(-tailbytes, 2)
     362               
     363            data = f.read()
     364        finally:
     365            f.close()
     366
     367        return data
     368
     369    def system_info(self, notes):
     370        """Return a string with laptop serial, battery type, build, memory info, etc."""
     371
     372        s = ''       
     373        try:
     374            # Do not include UUID!
     375            s += 'laptop-info-version: 1.1\n'
     376           
     377            s += '\n[Notes]\n%s\n' % notes
     378           
     379            s += '\n[System]\n'
     380            s += 'sugar_version: %s\n' % self._mp.sugar_version()
     381            s += 'release_id: %s\n' % self._mp.release_id()
     382            s += 'user_nick: %s\n' % self._mp.user_nick()
     383            s += 'clock: %f\n' % time.clock()
     384            s += 'date: %s\n' % time.strftime("%a, %d %b %Y %H:%M:%S +0000",
     385                                              time.gmtime())
     386            s += 'memfree: %s\n' % self._mp.memfree()
     387            s += 'disksize: %s MB\n' % ( self._mp.disksize('/') / (1024*1024) )
     388            s += 'diskfree: %s MB\n' % ( self._mp.diskfree('/') / (1024*1024) )
     389            s += 'kernel_version: %s\n' % self._mp.kernel_version()
     390            s += 'uptime: %s\n' % self._mp.uptime()
     391            s += 'loadavg: %s\n' % self._mp.loadavg()
     392           
     393            s += '\n[OFW]\n'
     394            s += 'serial-number: %s\n' % self._mp.ofw_serial_number()
     395            s += 'motherboard-number: %s\n' % self._mp.ofw_motherboard_number()
     396            s += 'board-revision: %s\n' %  self._mp.ofw_board_revision()
     397            s += 'keyboard: %s\n' %  self._mp.ofw_keyboard()
     398            s += 'wireless_mac: %s\n' %  self._mp.ofw_wireless_mac()
     399            s += 'firmware: %s\n' %  self._mp.ofw_bios_version()
     400            s += 'country: %s\n' % self._mp.ofw_country()
     401            s += 'localization: %s\n' % self._mp.ofw_localization()
     402               
     403            s += '\n[XO Battery]\n%s\n' % self._mp.xo_battery_info()
     404           
     405            s += '\n[Activities]\n%s\n' % self._mp.installed_activities()
     406           
     407            s += "\n[/sbin/ifconfig]\n%s\n" % self._mp.ifconfig()
     408            s += "\n[/sbin/route -n]\n%s\n" % self._mp.route_n()
     409           
     410            s += '\n[df -a]\n%s\n' % self._mp.df_a()
     411            s += '\n[ps auxwww]\n%s\n' % self._mp.ps_auxfwww()
     412            s += '\n[free]\n%s\n' % self._mp.usr_bin_free()
     413            s += '\n[top -bn2]\n%s\n' % self._mp.top()
     414       
     415        except Exception, e:
     416            s += '\nException while building info:\n%s\n' % e
     417       
     418        return s
     419
     420class LogSend:
     421   
     422    # post_multipart and encode_multipart_formdata have been taken from
     423    #  http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
     424    def post_multipart(self, host, selector, fields, files):
     425        """
     426        Post fields and files to an http host as multipart/form-data.
     427        fields is a sequence of (name, value) elements for regular form fields.
     428        files is a sequence of (name, filename, value) elements for data to be uploaded as files
     429        Return the server's response page.       
     430        """
     431        content_type, body = self.encode_multipart_formdata(fields, files)
     432        h = httplib.HTTP(host)
     433        h.putrequest('POST', selector)
     434        h.putheader('content-type', content_type)
     435        h.putheader('content-length', str(len(body)))
     436        h.putheader('Host', host)
     437        h.endheaders()
     438        h.send(body)
     439        errcode, errmsg, headers = h.getreply()
     440        return h.file.read()
     441   
     442    def encode_multipart_formdata(self, fields, files):
     443        """
     444        fields is a sequence of (name, value) elements for regular form fields.
     445        files is a sequence of (name, filename, value) elements for data to be uploaded as files
     446        Return (content_type, body) ready for httplib.HTTP instance
     447        """
     448        BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
     449        CRLF = '\r\n'
     450        L = []
     451        for (key, value) in fields:
     452            L.append('--' + BOUNDARY)
     453            L.append('Content-Disposition: form-data; name="%s"' % key)
     454            L.append('')
     455            L.append(value)
     456        for (key, filename, value) in files:
     457            L.append('--' + BOUNDARY)
     458            L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
     459            L.append('Content-Type: %s' % self.get_content_type(filename))
     460            L.append('')
     461            L.append(value)
     462        L.append('--' + BOUNDARY + '--')
     463        L.append('')
     464        body = CRLF.join(L)
     465        content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
     466        return content_type, body
     467   
     468    def read_file(self, filename):
     469        """Read the entire contents of a file and return it as a string"""
     470
     471        data = ''
     472
     473        f = open(filename)
     474        try:
     475            data = f.read()
     476        finally:
     477            f.close()
     478
     479        return data
     480
     481    def get_content_type(self, filename):
     482        return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
     483       
     484    def http_post_logs(self, url, archive, notes=''):
     485        #host, selector, fields, files
     486        files = ('logs', os.path.basename(archive), self.read_file(archive)),
     487       
     488        fields = ('notes', notes),
     489        urlparts = urlparse.urlsplit(url)
     490        _logger.debug("Sending logs to %s" % url)
     491        r = self.post_multipart(urlparts[1], urlparts[2], fields, files)
     492       
     493        _logger.debug(r)
     494       
     495        # Parse server response       
     496        result = False
     497        id = None
     498       
     499        lines = r.splitlines()
     500        for line in lines:
     501            if line.startswith('ID='):
     502                id = line[3:]
     503            elif line.startswith('OK'):
     504                result = True
     505       
     506        return result, id
  • new file extensions/cpsection/report/model.py

    diff --git a/extensions/cpsection/report/model.py b/extensions/cpsection/report/model.py
    new file mode 100644
    index 0000000..04da499
    - +  
     1# Copyright (C) 2008 One Laptop Per Child
     2#
     3# This program is free software; you can redistribute it and/or modify
     4# it under the terms of the GNU General Public License as published by
     5# the Free Software Foundation; either version 2 of the License, or
     6# (at your option) any later version.
     7#
     8# This program is distributed in the hope that it will be useful,
     9# but WITHOUT ANY WARRANTY; without even the implied warranty of
     10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     11# GNU General Public License for more details.
     12#
     13# You should have received a copy of the GNU General Public License
     14# along with this program; if not, write to the Free Software
     15# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
     16#
     17
     18from gettext import gettext as _
     19
     20import os
     21import logging
     22import logcollect
     23import gconf
     24
     25from sugar.datastore import datastore
     26
     27_logger = logging.getLogger('LogCollect')
     28
     29def save_logs_to_journal(archive_name):
     30    _logger.debug('Saving %s to the journal.' % archive_name)
     31
     32    ds = datastore.create()
     33
     34    ds.metadata['title'] = archive_name
     35    ds.metadata['mime_type'] = 'application/zip'
     36   
     37    ds.file_path = archive_name
     38   
     39    datastore.write(ds, transfer_ownership=True)
     40   
     41    ds.destroy()
     42    del ds
     43
     44def send_logs(notes):
     45    try:
     46        collector = logcollect.LogCollect()
     47        archive = collector.write_logs(notes)
     48
     49    except Exception, e:
     50        _logger.debug('Failed to collect logs:\n%s\n', e)
     51        return False, 0
     52
     53    try:
     54        client = gconf.client_get_default()
     55        server = client.get_string('/desktop/sugar/logcollect_server')
     56        if server[:7] != 'http://':
     57            server = 'http://' + server + '/'
     58
     59        sender = logcollect.LogSend()
     60        return sender.http_post_logs(server, archive, notes)
     61
     62    except Exception, e:
     63        _logger.debug('Failed to upload logs:\n%s\n', e)
     64
     65        # When the upload fails, fall back to saving the logs to the Journal.
     66        try:
     67            save_logs_to_journal(archive)
     68       
     69        except Exception, e:
     70            _logger.debug('Failed to save logs to journal:\n%s\n', e)
     71
     72        return False, 0
  • new file extensions/cpsection/report/view.py

    diff --git a/extensions/cpsection/report/view.py b/extensions/cpsection/report/view.py
    new file mode 100644
    index 0000000..cfe8806
    - +  
     1# Copyright (C) 2008, OLPC
     2#
     3# This program is free software; you can redistribute it and/or modify
     4# it under the terms of the GNU General Public License as published by
     5# the Free Software Foundation; either version 2 of the License, or
     6# (at your option) any later version.
     7#
     8# This program is distributed in the hope that it will be useful,
     9# but WITHOUT ANY WARRANTY; without even the implied warranty of
     10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     11# GNU General Public License for more details.
     12#
     13# You should have received a copy of the GNU General Public License
     14# along with this program; if not, write to the Free Software
     15# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
     16
     17import gtk
     18from gettext import gettext as _
     19
     20from sugar.graphics import style
     21
     22from jarabe.controlpanel.sectionview import SectionView
     23from jarabe.controlpanel.inlinealert import InlineAlert
     24
     25import logcollect
     26
     27class ProblemReport(SectionView):
     28    def __init__(self, model, alerts):
     29        SectionView.__init__(self)
     30
     31        self._model = model
     32        self.restart_alerts = alerts
     33
     34        self.set_border_width(style.DEFAULT_SPACING * 2)
     35        self.set_spacing(style.DEFAULT_SPACING)
     36       
     37        description_label = gtk.Label(_('Please describe the problem you encountered.'))
     38        description_label.show()
     39       
     40        self.description_text = gtk.TextView(gtk.TextBuffer())
     41        self.description_text.props.wrap_mode = gtk.WRAP_WORD
     42        self.description_text.show()
     43        #self.description_text.grab_focus()
     44       
     45        description_scroll = gtk.ScrolledWindow()
     46        description_scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
     47        description_scroll.add(self.description_text)
     48        description_scroll.show()
     49       
     50        description_frame = gtk.Frame()
     51        description_frame.add(description_scroll)
     52        description_frame.show()
     53       
     54        upload_warning = gtk.Label()
     55        upload_warning.set_markup(_('<b>Caution:</b> The diagnostic logs attached to the report may contain personal data.'))
     56        upload_warning.show()
     57       
     58        upload_label = gtk.Label(_('Upload report'))
     59        upload_label.show()
     60       
     61        upload_button = gtk.Button()
     62        upload_button.add(upload_label)
     63        upload_button.show()
     64       
     65        upload_button.connect('clicked', self.__upload_button_clicked)
     66       
     67        self.result_label = gtk.Label(' ')
     68        self.result_label.show()
     69       
     70        self.pack_start(description_label, expand=False)
     71        self.pack_start(description_frame, expand=True)
     72        self.pack_start(upload_warning, expand=False)
     73        self.pack_start(upload_button, expand=False)
     74        self.pack_start(self.result_label, expand=False)
     75       
     76        self.setup()
     77
     78    def setup(self):
     79        pass
     80
     81    def undo(self):
     82        pass
     83
     84    def __upload_button_clicked(self, button):
     85        self.result_label.set_text(' ')
     86       
     87        buf = self.description_text.get_buffer()
     88        description = buf.get_text(buf.get_start_iter(), buf.get_end_iter())
     89       
     90        result, id = self._model.send_logs(description)
     91       
     92        if result:
     93            self.result_label.set_markup(_('The report was uploaded to the server.  The report ID is %s.') % id)
     94        else:
     95            self.result_label.set_markup(_('The report could not be uploaded to the server.  A copy was saved to the Journal.'))