Back to index

apport  2.4
hookutils.py
Go to the documentation of this file.
00001 '''Convenience functions for use in package hooks.'''
00002 
00003 # Copyright (C) 2008 - 2012 Canonical Ltd.
00004 # Authors:
00005 #   Matt Zimmerman <mdz@canonical.com>
00006 #   Brian Murray <brian@ubuntu.com>
00007 #   Martin Pitt <martin.pitt@ubuntu.com>
00008 #
00009 # This program is free software; you can redistribute it and/or modify it
00010 # under the terms of the GNU General Public License as published by the
00011 # Free Software Foundation; either version 2 of the License, or (at your
00012 # option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
00013 # the full text of the license.
00014 
00015 import subprocess
00016 import os
00017 import sys
00018 import time
00019 import calendar
00020 import datetime
00021 import glob
00022 import re
00023 import stat
00024 import base64
00025 import tempfile
00026 import shutil
00027 import locale
00028 
00029 from gi.repository import Gio, GLib
00030 
00031 from apport.packaging_impl import impl as packaging
00032 
00033 import apport
00034 
00035 try:
00036     _path_key_trans = ''.maketrans('#/-_+ ', '....._')
00037 except AttributeError:
00038     # Python 2 variant
00039     import string
00040     _path_key_trans = string.maketrans('#/-_+ ', '....._')
00041 
00042 
00043 def path_to_key(path):
00044     '''Generate a valid report key name from a file path.
00045 
00046     This will replace invalid punctuation symbols with valid ones.
00047     '''
00048     if sys.version[0] >= '3':
00049         if type(path) == type(b''):
00050             path = path.decode('UTF-8')
00051     else:
00052         if type(path) != type(b''):
00053             path = path.encode('UTF-8')
00054     return path.translate(_path_key_trans)
00055 
00056 
00057 def attach_file_if_exists(report, path, key=None, overwrite=True, force_unicode=False):
00058     '''Attach file contents if file exists.
00059 
00060     If key is not specified, the key name will be derived from the file
00061     name with path_to_key().
00062 
00063     If overwrite is True, an existing key will be updated. If it is False, a
00064     new key with '_' appended will be added instead.
00065 
00066     If the contents is valid UTF-8, or force_unicode is True, then the value
00067     will a string, otherwise it will be bytes.
00068     '''
00069     if not key:
00070         key = path_to_key(path)
00071 
00072     if os.path.exists(path):
00073         attach_file(report, path, key, overwrite, force_unicode)
00074 
00075 
00076 def read_file(path, force_unicode=False):
00077     '''Return the contents of the specified path.
00078 
00079     If the contents is valid UTF-8, or force_unicode is True, then the value
00080     will a string, otherwise it will be bytes.
00081 
00082     Upon error, this will deliver a text representation of the error,
00083     instead of failing.
00084     '''
00085     try:
00086         with open(path, 'rb') as f:
00087             contents = f.read().strip()
00088         if force_unicode:
00089             return contents.decode('UTF-8', errors='replace')
00090         try:
00091             return contents.decode('UTF-8')
00092         except UnicodeDecodeError:
00093             return contents
00094     except Exception as e:
00095         return 'Error: ' + str(e)
00096 
00097 
00098 def attach_file(report, path, key=None, overwrite=True, force_unicode=False):
00099     '''Attach a file to the report.
00100 
00101     If key is not specified, the key name will be derived from the file
00102     name with path_to_key().
00103 
00104     If overwrite is True, an existing key will be updated. If it is False, a
00105     new key with '_' appended will be added instead.
00106 
00107     If the contents is valid UTF-8, or force_unicode is True, then the value
00108     will a string, otherwise it will be bytes.
00109     '''
00110     if not key:
00111         key = path_to_key(path)
00112 
00113     # Do not clobber existing keys
00114     if not overwrite:
00115         while key in report:
00116             key += '_'
00117     report[key] = read_file(path, force_unicode=force_unicode)
00118 
00119 
00120 def attach_conffiles(report, package, conffiles=None, ui=None):
00121     '''Attach information about any modified or deleted conffiles.
00122 
00123     If conffiles is given, only this subset will be attached. If ui is given,
00124     ask whether the contents of the file may be added to the report; if this is
00125     denied, or there is no UI, just mark it as "modified" in the report.
00126     '''
00127     modified = packaging.get_modified_conffiles(package)
00128 
00129     for path, contents in modified.items():
00130         if conffiles and path not in conffiles:
00131             continue
00132 
00133         key = 'modified.conffile.' + path_to_key(path)
00134         if contents == '[deleted]':
00135             report[key] = contents
00136             continue
00137 
00138         if ui:
00139             response = ui.yesno('It seems you have modified the contents of "%s".  Would you like to add the contents of it to your bug report?' % path)
00140             if response:
00141                 report[key] = contents
00142             else:
00143                 report[key] = '[modified]'
00144         else:
00145             report[key] = '[modified]'
00146 
00147         mtime = datetime.datetime.fromtimestamp(os.stat(path).st_mtime)
00148         report['mtime.conffile.' + path_to_key(path)] = mtime.isoformat()
00149 
00150 
00151 def attach_upstart_overrides(report, package):
00152     '''Attach information about any Upstart override files'''
00153 
00154     try:
00155         files = apport.packaging.get_files(package)
00156     except ValueError:
00157         return
00158 
00159     for file in files:
00160         if os.path.exists(file) and file.startswith('/etc/init/'):
00161             override = file.replace('.conf', '.override')
00162             key = 'upstart.' + override.replace('/etc/init/', '')
00163             attach_file_if_exists(report, override, key)
00164 
00165 
00166 def attach_dmesg(report):
00167     '''Attach information from the kernel ring buffer (dmesg).
00168 
00169     This will not overwrite already existing information.
00170     '''
00171     try:
00172         if not report.get('BootDmesg', '').strip():
00173             with open('/var/log/dmesg') as f:
00174                 report['BootDmesg'] = f.read()
00175     except IOError:
00176         pass
00177     if not report.get('CurrentDmesg', '').strip():
00178         dmesg = command_output(['sh', '-c', 'dmesg | comm -13 --nocheck-order /var/log/dmesg -'])
00179         # if an initial message was truncated by the ring buffer, skip over it
00180         first_newline = dmesg.find('\n[')
00181         if first_newline != -1:
00182             dmesg = dmesg[first_newline + 1:]
00183         report['CurrentDmesg'] = dmesg
00184 
00185 
00186 def attach_dmi(report):
00187     dmi_dir = '/sys/class/dmi/id'
00188     if os.path.isdir(dmi_dir):
00189         for f in os.listdir(dmi_dir):
00190             p = '%s/%s' % (dmi_dir, f)
00191             st = os.stat(p)
00192             # ignore the root-only ones, since they have serial numbers
00193             if not stat.S_ISREG(st.st_mode) or (st.st_mode & 4 == 0):
00194                 continue
00195             if f in ('subsystem', 'uevent'):
00196                 continue
00197 
00198             try:
00199                 with open(p) as fd:
00200                     value = fd.read().strip()
00201             except (OSError, IOError):
00202                 continue
00203             if value:
00204                 report['dmi.' + f.replace('_', '.')] = value
00205 
00206 
00207 def attach_hardware(report):
00208     '''Attach a standard set of hardware-related data to the report, including:
00209 
00210     - kernel dmesg (boot and current)
00211     - /proc/interrupts
00212     - /proc/cpuinfo
00213     - /proc/cmdline
00214     - /proc/modules
00215     - lspci -vvnn
00216     - lsusb
00217     - devices from udev
00218     - DMI information from /sys
00219     - prtconf (sparc)
00220     - pccardctl status/ident
00221     '''
00222     attach_dmesg(report)
00223 
00224     attach_file(report, '/proc/interrupts', 'ProcInterrupts')
00225     attach_file(report, '/proc/cpuinfo', 'ProcCpuinfo')
00226     attach_file(report, '/proc/cmdline', 'ProcKernelCmdLine')
00227     attach_file(report, '/var/log/udev', 'UdevLog', force_unicode=True)
00228 
00229     if os.path.exists('/sys/bus/pci'):
00230         report['Lspci'] = command_output(['lspci', '-vvnn'])
00231     report['Lsusb'] = command_output(['lsusb'])
00232     report['ProcModules'] = command_output(['sort', '/proc/modules'])
00233     report['UdevDb'] = command_output(['udevadm', 'info', '--export-db'])
00234 
00235     # anonymize partition labels
00236     l = report['UdevLog']
00237     l = re.sub('ID_FS_LABEL=(.*)', 'ID_FS_LABEL=<hidden>', l)
00238     l = re.sub('ID_FS_LABEL_ENC=(.*)', 'ID_FS_LABEL_ENC=<hidden>', l)
00239     l = re.sub('by-label/(.*)', 'by-label/<hidden>', l)
00240     l = re.sub('ID_FS_LABEL=(.*)', 'ID_FS_LABEL=<hidden>', l)
00241     l = re.sub('ID_FS_LABEL_ENC=(.*)', 'ID_FS_LABEL_ENC=<hidden>', l)
00242     l = re.sub('by-label/(.*)', 'by-label/<hidden>', l)
00243     report['UdevLog'] = l
00244 
00245     attach_dmi(report)
00246 
00247     # Use the hardware information to create a machine type.
00248     if 'dmi.sys.vendor' in report and 'dmi.product.name' in report:
00249         report['MachineType'] = '%s %s' % (report['dmi.sys.vendor'],
00250                                            report['dmi.product.name'])
00251 
00252     if command_available('prtconf'):
00253         report['Prtconf'] = command_output(['prtconf'])
00254 
00255     if command_available('pccardctl'):
00256         out = command_output(['pccardctl', 'status']).strip()
00257         if out:
00258             report['PccardctlStatus'] = out
00259         out = command_output(['pccardctl', 'ident']).strip()
00260         if out:
00261             report['PccardctlIdent'] = out
00262 
00263 
00264 def attach_alsa_old(report):
00265     ''' (loosely based on http://www.alsa-project.org/alsa-info.sh)
00266     for systems where alsa-info is not installed (i e, *buntu 12.04 and earlier)
00267     '''
00268     attach_file_if_exists(report, os.path.expanduser('~/.asoundrc'),
00269                           'UserAsoundrc')
00270     attach_file_if_exists(report, os.path.expanduser('~/.asoundrc.asoundconf'),
00271                           'UserAsoundrcAsoundconf')
00272     attach_file_if_exists(report, '/etc/asound.conf')
00273     attach_file_if_exists(report, '/proc/asound/version', 'AlsaVersion')
00274     attach_file(report, '/proc/cpuinfo', 'ProcCpuinfo')
00275 
00276     report['AlsaDevices'] = command_output(['ls', '-l', '/dev/snd/'])
00277     report['AplayDevices'] = command_output(['aplay', '-l'])
00278     report['ArecordDevices'] = command_output(['arecord', '-l'])
00279 
00280     report['PciMultimedia'] = pci_devices(PCI_MULTIMEDIA)
00281 
00282     cards = []
00283     if os.path.exists('/proc/asound/cards'):
00284         with open('/proc/asound/cards') as fd:
00285             for line in fd:
00286                 if ']:' in line:
00287                     fields = line.lstrip().split()
00288                     cards.append(int(fields[0]))
00289 
00290     for card in cards:
00291         key = 'Card%d.Amixer.info' % card
00292         report[key] = command_output(['amixer', '-c', str(card), 'info'])
00293         key = 'Card%d.Amixer.values' % card
00294         report[key] = command_output(['amixer', '-c', str(card)])
00295 
00296         for codecpath in glob.glob('/proc/asound/card%d/codec*' % card):
00297             if os.path.isfile(codecpath):
00298                 codec = os.path.basename(codecpath)
00299                 key = 'Card%d.Codecs.%s' % (card, path_to_key(codec))
00300                 attach_file(report, codecpath, key=key)
00301             elif os.path.isdir(codecpath):
00302                 codec = os.path.basename(codecpath)
00303                 for name in os.listdir(codecpath):
00304                     path = os.path.join(codecpath, name)
00305                     key = 'Card%d.Codecs.%s.%s' % (card, path_to_key(codec), path_to_key(name))
00306                     attach_file(report, path, key)
00307 
00308 
00309 def attach_alsa(report):
00310     '''Attach ALSA subsystem information to the report.
00311     '''
00312     if os.path.exists('/usr/share/alsa-base/alsa-info.sh'):
00313         report['AlsaInfo'] = command_output(['/usr/share/alsa-base/alsa-info.sh', '--stdout', '--no-upload'])
00314     else:
00315         attach_alsa_old(report)
00316 
00317     report['AudioDevicesInUse'] = command_output(
00318         ['fuser', '-v'] + glob.glob('/dev/dsp*') + glob.glob('/dev/snd/*') + glob.glob('/dev/seq*'))
00319 
00320     if os.path.exists('/usr/bin/pacmd'):
00321         report['PulseList'] = command_output(['pacmd', 'list'])
00322 
00323     attach_dmi(report)
00324     attach_dmesg(report)
00325 
00326 
00327 def command_available(command):
00328     '''Is given command on the executable search path?'''
00329     if 'PATH' not in os.environ:
00330         return False
00331     path = os.environ['PATH']
00332     for element in path.split(os.pathsep):
00333         if not element:
00334             continue
00335         filename = os.path.join(element, command)
00336         if os.path.isfile(filename) and os.access(filename, os.X_OK):
00337             return True
00338     return False
00339 
00340 
00341 def command_output(command, input=None, stderr=subprocess.STDOUT,
00342                    keep_locale=False, decode_utf8=True):
00343     '''Try to execute given command (array) and return its stdout.
00344 
00345     In case of failure, a textual error gets returned. This function forces
00346     LC_MESSAGES to C, to avoid translated output in bug reports.
00347 
00348     If decode_utf8 is True (default), the output will be converted to a string,
00349     otherwise left as bytes.
00350     '''
00351     env = os.environ.copy()
00352     if not keep_locale:
00353         env['LC_MESSAGES'] = 'C'
00354     try:
00355         sp = subprocess.Popen(command, stdout=subprocess.PIPE,
00356                               stderr=stderr,
00357                               stdin=(input and subprocess.PIPE or None),
00358                               env=env)
00359     except OSError as e:
00360         return 'Error: ' + str(e)
00361 
00362     out = sp.communicate(input)[0]
00363     if sp.returncode == 0:
00364         res = out.strip()
00365     else:
00366         res = (b'Error: command ' + str(command).encode() + b' failed with exit code '
00367                + str(sp.returncode).encode() + b': ' + out)
00368 
00369     if decode_utf8:
00370         res = res.decode('UTF-8', errors='replace')
00371     return res
00372 
00373 
00374 def _root_command_prefix():
00375     if os.getuid() == 0:
00376         prefix = []
00377     elif os.getenv('DISPLAY') and \
00378             subprocess.call(['which', 'kdesudo'], stdout=subprocess.PIPE,
00379                             stderr=subprocess.PIPE) == 0 and \
00380             subprocess.call(['pgrep', '-x', '-u', str(os.getuid()), 'ksmserver'],
00381                             stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0:
00382         prefix = ['kdesudo', '--desktop', '/usr/share/applications/apport-kde-mime.desktop',
00383                   '--', 'env', '-u', 'LANGUAGE', 'LC_MESSAGES=C']
00384     elif os.getenv('DISPLAY') and \
00385             subprocess.call(['which', 'gksu'], stdout=subprocess.PIPE,
00386                             stderr=subprocess.PIPE) == 0 and \
00387             subprocess.call(['pgrep', '-x', '-u', str(os.getuid()), 'gnome-panel|gconfd-2'],
00388                             stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0:
00389         prefix = ['gksu', '-D', 'Apport', '--', 'env', '-u', 'LANGUAGE', 'LC_MESSAGES=C']
00390     else:
00391         prefix = ['sudo', 'LC_MESSAGES=C', 'LANGUAGE=']
00392 
00393     return prefix
00394 
00395 
00396 def root_command_output(command, input=None, stderr=subprocess.STDOUT, decode_utf8=True):
00397     '''Try to execute given command (array) as root and return its stdout.
00398 
00399     This passes the command through gksu, kdesudo, or sudo, depending on the
00400     running desktop environment.
00401 
00402     In case of failure, a textual error gets returned.
00403 
00404     If decode_utf8 is True (default), the output will be converted to a string,
00405     otherwise left as bytes.
00406     '''
00407     assert isinstance(command, list), 'command must be a list'
00408     return command_output(_root_command_prefix() + command, input, stderr,
00409                           keep_locale=True, decode_utf8=decode_utf8)
00410 
00411 
00412 def attach_root_command_outputs(report, command_map):
00413     '''Execute multiple commands as root and put their outputs into report.
00414 
00415     command_map is a keyname -> 'shell command' dictionary with the commands to
00416     run. They are all run through /bin/sh, so you need to take care of shell
00417     escaping yourself. To include stderr output of a command, end it with
00418     "2>&1".
00419 
00420     Just like root_command_output() this will use gksu, kdesudo, or sudo for
00421     gaining root privileges, depending on the running desktop environment.
00422 
00423     This is preferrable to using root_command_output() multiple times, as that
00424     will ask for the password every time.
00425     '''
00426     workdir = tempfile.mkdtemp()
00427     try:
00428         # create a shell script with all the commands
00429         script_path = os.path.join(workdir, ':script:')
00430         script = open(script_path, 'w')
00431         for keyname, command in command_map.items():
00432             assert hasattr(command, 'strip'), 'command must be a string (shell command)'
00433             # use "| cat" here, so that we can end commands with 2>&1
00434             # (otherwise it would have the wrong redirection order)
00435             script.write('%s | cat > %s\n' % (command, os.path.join(workdir, keyname)))
00436         script.close()
00437 
00438         # run script
00439         sp = subprocess.Popen(_root_command_prefix() + ['/bin/sh', script_path])
00440         sp.wait()
00441 
00442         # now read back the individual outputs
00443         for keyname in command_map:
00444             f = open(os.path.join(workdir, keyname))
00445             buf = f.read().strip()
00446             if buf:
00447                 report[keyname] = buf
00448             f.close()
00449     finally:
00450         shutil.rmtree(workdir)
00451 
00452 
00453 def recent_syslog(pattern):
00454     '''Extract recent messages from syslog which match a regex.
00455 
00456     pattern should be a "re" object.
00457     '''
00458     return recent_logfile('/var/log/syslog', pattern)
00459 
00460 
00461 def recent_logfile(logfile, pattern, maxlines=10000):
00462     '''Extract recent messages from a logfile which match a regex.
00463 
00464     pattern should be a "re" object. By default this catches at most the last
00465     1000 lines, but this can be modified with a different maxlines argument.
00466     '''
00467     lines = ''
00468     try:
00469         tail = subprocess.Popen(['tail', '-n', str(maxlines), logfile],
00470                                 stdout=subprocess.PIPE)
00471         while tail.poll() is None:
00472             for line in tail.stdout:
00473                 line = line.decode('UTF-8', errors='replace')
00474                 if pattern.search(line):
00475                     lines += line
00476         tail.stdout.close()
00477         tail.wait()
00478     except IOError:
00479         return ''
00480     return lines
00481 
00482 
00483 def xsession_errors(pattern=None):
00484     '''Extract messages from ~/.xsession-errors.
00485 
00486     By default this parses out glib-style warnings, errors, criticals etc. and
00487     X window errors.  You can specify a "re" object as pattern to customize the
00488     filtering.
00489 
00490     Please note that you should avoid attaching the whole file to reports, as
00491     it can, and often does, contain sensitive and private data.
00492     '''
00493     path = os.path.expanduser('~/.xsession-errors')
00494     if not os.path.exists(path):
00495         return ''
00496 
00497     if not pattern:
00498         pattern = re.compile('^(\(.*:\d+\): \w+-(WARNING|CRITICAL|ERROR))|(Error: .*No Symbols named)|([^ ]+\[\d+\]: ([A-Z]+):)|([^ ]-[A-Z]+ \*\*:)|(received an X Window System error)|(^The error was \')|(^  \(Details: serial \d+ error_code)')
00499 
00500     lines = ''
00501     with open(path, 'rb') as f:
00502         for line in f:
00503             line = line.decode('UTF-8', errors='replace')
00504             if pattern.search(line):
00505                 lines += line
00506     return lines
00507 
00508 PCI_MASS_STORAGE = 0x01
00509 PCI_NETWORK = 0x02
00510 PCI_DISPLAY = 0x03
00511 PCI_MULTIMEDIA = 0x04
00512 PCI_MEMORY = 0x05
00513 PCI_BRIDGE = 0x06
00514 PCI_SIMPLE_COMMUNICATIONS = 0x07
00515 PCI_BASE_SYSTEM_PERIPHERALS = 0x08
00516 PCI_INPUT_DEVICES = 0x09
00517 PCI_DOCKING_STATIONS = 0x0a
00518 PCI_PROCESSORS = 0x0b
00519 PCI_SERIAL_BUS = 0x0c
00520 
00521 
00522 def pci_devices(*pci_classes):
00523     '''Return a text dump of PCI devices attached to the system.'''
00524 
00525     if not pci_classes:
00526         return command_output(['lspci', '-vvnn'])
00527 
00528     result = ''
00529     output = command_output(['lspci', '-vvmmnn'])
00530     for paragraph in output.split('\n\n'):
00531         pci_class = None
00532         slot = None
00533 
00534         for line in paragraph.split('\n'):
00535             try:
00536                 key, value = line.split(':', 1)
00537             except ValueError:
00538                 continue
00539             value = value.strip()
00540             key = key.strip()
00541             if key == 'Class':
00542                 n = int(value[-5:-1], 16)
00543                 pci_class = (n & 0xff00) >> 8
00544             elif key == 'Slot':
00545                 slot = value
00546 
00547         if pci_class and slot and pci_class in pci_classes:
00548             if result:
00549                 result += '\n\n'
00550             result += command_output(['lspci', '-vvnns', slot]).strip()
00551 
00552     return result
00553 
00554 
00555 def usb_devices():
00556     '''Return a text dump of USB devices attached to the system.'''
00557 
00558     # TODO: would be nice to be able to filter by interface class
00559     return command_output(['lsusb', '-v'])
00560 
00561 
00562 def files_in_package(package, globpat=None):
00563     '''Retrieve a list of files owned by package, optionally matching globpat'''
00564 
00565     files = packaging.get_files(package)
00566     if globpat:
00567         result = [f for f in files if glob.fnmatch.fnmatch(f, globpat)]
00568     else:
00569         result = files
00570     return result
00571 
00572 
00573 def attach_gconf(report, package):
00574     '''Obsolete'''
00575 
00576     # keeping a no-op function for some time to not break hooks
00577     pass
00578 
00579 
00580 def attach_gsettings_schema(report, schema):
00581     '''Attach user-modified gsttings keys of a schema.'''
00582 
00583     cur_value = report.get('GsettingsChanges', '')
00584 
00585     defaults = {}  # schema -> key ->  value
00586     env = os.environ.copy()
00587     env['XDG_CONFIG_HOME'] = '/nonexisting'
00588     gsettings = subprocess.Popen(['gsettings', 'list-recursively', schema],
00589                                  env=env, stdout=subprocess.PIPE)
00590     for l in gsettings.stdout:
00591         try:
00592             (schema, key, value) = l.split(None, 2)
00593             value = value.rstrip()
00594         except ValueError:
00595             continue  # invalid line
00596         defaults.setdefault(schema, {})[key] = value
00597 
00598     gsettings = subprocess.Popen(['gsettings', 'list-recursively', schema],
00599                                  stdout=subprocess.PIPE)
00600     for l in gsettings.stdout:
00601         try:
00602             (schema, key, value) = l.split(None, 2)
00603             value = value.rstrip()
00604         except ValueError:
00605             continue  # invalid line
00606 
00607         if value != defaults.get(schema, {}).get(key, ''):
00608             cur_value += '%s %s %s\n' % (schema, key, value)
00609 
00610     report['GsettingsChanges'] = cur_value
00611 
00612 
00613 def attach_gsettings_package(report, package):
00614     '''Attach user-modified gsettings keys of all schemas in a package.'''
00615 
00616     for schema_file in files_in_package(package, '/usr/share/glib-2.0/schemas/*.gschema.xml'):
00617         schema = os.path.basename(schema_file)[:-12]
00618         attach_gsettings_schema(report, schema)
00619 
00620 
00621 def attach_network(report):
00622     '''Attach generic network-related information to report.'''
00623 
00624     report['IpRoute'] = command_output(['ip', 'route'])
00625     report['IpAddr'] = command_output(['ip', 'addr'])
00626     report['PciNetwork'] = pci_devices(PCI_NETWORK)
00627     attach_file_if_exists(report, '/etc/network/interfaces', key='IfupdownConfig')
00628 
00629     for var in ('http_proxy', 'ftp_proxy', 'no_proxy'):
00630         if var in os.environ:
00631             report[var] = os.environ[var]
00632 
00633 
00634 def attach_wifi(report):
00635     '''Attach wireless (WiFi) network information to report.'''
00636 
00637     report['WifiSyslog'] = recent_syslog(re.compile(r'(NetworkManager|modem-manager|dhclient|kernel|wpa_supplicant)(\[\d+\])?:'))
00638     report['IwConfig'] = re.sub(
00639         'ESSID:(.*)', 'ESSID:<hidden>',
00640         re.sub('Encryption key:(.*)', 'Encryption key: <hidden>',
00641                re.sub('Access Point: (.*)', 'Access Point: <hidden>',
00642                       command_output(['iwconfig']))))
00643     report['RfKill'] = command_output(['rfkill', 'list'])
00644     report['CRDA'] = command_output(['iw', 'reg', 'get'])
00645 
00646     attach_file_if_exists(report, '/var/log/wpa_supplicant.log', key='WpaSupplicantLog')
00647 
00648 
00649 def attach_printing(report):
00650     '''Attach printing information to the report.
00651 
00652     Based on http://wiki.ubuntu.com/PrintingBugInfoScript.
00653     '''
00654     attach_file_if_exists(report, '/etc/papersize', 'Papersize')
00655     attach_file_if_exists(report, '/var/log/cups/error_log', 'CupsErrorLog')
00656     report['Locale'] = command_output(['locale'])
00657     report['Lpstat'] = command_output(['lpstat', '-v'])
00658 
00659     ppds = glob.glob('/etc/cups/ppd/*.ppd')
00660     if ppds:
00661         nicknames = command_output(['fgrep', '-H', '*NickName'] + ppds)
00662         report['PpdFiles'] = re.sub('/etc/cups/ppd/(.*).ppd:\*NickName: *"(.*)"', '\g<1>: \g<2>', nicknames)
00663 
00664     report['PrintingPackages'] = package_versions(
00665         'foo2zjs', 'foomatic-db', 'foomatic-db-engine',
00666         'foomatic-db-gutenprint', 'foomatic-db-hpijs', 'foomatic-filters',
00667         'foomatic-gui', 'hpijs', 'hplip', 'm2300w', 'min12xxw', 'c2050',
00668         'hpoj', 'pxljr', 'pnm2ppa', 'splix', 'hp-ppd', 'hpijs-ppds',
00669         'linuxprinting.org-ppds', 'openprinting-ppds',
00670         'openprinting-ppds-extra', 'ghostscript', 'cups',
00671         'cups-driver-gutenprint', 'foomatic-db-gutenprint', 'ijsgutenprint',
00672         'cupsys-driver-gutenprint', 'gimp-gutenprint', 'gutenprint-doc',
00673         'gutenprint-locales', 'system-config-printer-common', 'kdeprint')
00674 
00675 
00676 def attach_mac_events(report):
00677     '''Attach MAC information and events to the report.'''
00678 
00679     mac_regex = 'audit\(|apparmor|selinux|security'
00680     mac_re = re.compile(mac_regex, re.IGNORECASE)
00681     aa_denied_regex = 'apparmor="DENIED"'
00682     aa_denied_re = re.compile(aa_denied_regex, re.IGNORECASE)
00683 
00684     if os.path.exists('/var/log/kern.log'):
00685         report['KernLog'] = recent_logfile('/var/log/kern.log', mac_re)
00686     elif os.path.exists('/var/log/messages'):
00687         report['KernLog'] = recent_logfile('/var/log/messages', mac_re)
00688 
00689     if os.path.exists('/var/run/auditd.pid'):
00690         attach_root_command_outputs(report, {'AuditLog': 'egrep "' + mac_regex + '" /var/log/audit/audit.log'})
00691 
00692     attach_file(report, '/proc/version_signature', 'ProcVersionSignature')
00693     attach_file(report, '/proc/cmdline', 'ProcCmdline')
00694 
00695     if re.search(aa_denied_re, report.get('KernLog', '')) or re.search(aa_denied_re, report.get('AuditLog', '')):
00696         tags = report.get('Tags', '')
00697         if tags:
00698             tags += ' '
00699         report['Tags'] = tags + 'apparmor'
00700 
00701 
00702 def attach_related_packages(report, packages):
00703     '''Attach version information for related packages
00704 
00705     In the future, this might also run their hooks.
00706     '''
00707     report['RelatedPackageVersions'] = package_versions(*packages)
00708 
00709 
00710 def package_versions(*packages):
00711     '''Return a text listing of package names and versions.
00712 
00713     Arguments may be package names or globs, e. g. "foo*"
00714     '''
00715     versions = []
00716     for package_pattern in packages:
00717         if not package_pattern:
00718             continue
00719 
00720         matching_packages = packaging.package_name_glob(package_pattern)
00721 
00722         if not matching_packages:
00723             versions.append((package_pattern, 'N/A'))
00724 
00725         for package in sorted(matching_packages):
00726             try:
00727                 version = packaging.get_version(package)
00728             except ValueError:
00729                 version = 'N/A'
00730             if version is None:
00731                 version = 'N/A'
00732             versions.append((package, version))
00733 
00734     package_width, version_width = \
00735         map(max, [map(len, t) for t in zip(*versions)])
00736 
00737     fmt = '%%-%ds %%s' % package_width
00738     return '\n'.join([fmt % v for v in versions])
00739 
00740 
00741 def shared_libraries(path):
00742     '''Returns a list of strings containing the sonames of shared libraries
00743     with which the specified binary is linked.'''
00744 
00745     libs = set()
00746 
00747     for line in command_output(['ldd', path]).split('\n'):
00748         try:
00749             lib, rest = line.split('=>', 1)
00750         except ValueError:
00751             continue
00752 
00753         lib = lib.strip()
00754         libs.add(lib)
00755 
00756     return libs
00757 
00758 
00759 def links_with_shared_library(path, lib):
00760     '''Returns True if the binary at path links with the library named lib.
00761 
00762     path should be a fully qualified path (e.g. report['ExecutablePath'])
00763     lib may be of the form 'lib<name>' or 'lib<name>.so.<version>'
00764     '''
00765 
00766     libs = shared_libraries(path)
00767 
00768     if lib in libs:
00769         return True
00770 
00771     for linked_lib in libs:
00772         if linked_lib.startswith(lib + '.so.'):
00773             return True
00774 
00775     return False
00776 
00777 
00778 def _get_module_license(module):
00779     '''Return the license for a given kernel module.'''
00780 
00781     try:
00782         modinfo = subprocess.Popen(['/sbin/modinfo', module],
00783                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)
00784         out = modinfo.communicate()[0].decode('UTF-8')
00785         if modinfo.returncode != 0:
00786             return 'invalid'
00787     except OSError:
00788         return None
00789     for l in out.splitlines():
00790         fields = l.split(':', 1)
00791         if len(fields) < 2:
00792             continue
00793         if fields[0] == 'license':
00794             return fields[1].strip()
00795 
00796     return None
00797 
00798 
00799 def nonfree_kernel_modules(module_list='/proc/modules'):
00800     '''Check loaded modules and return a list of those which are not free.'''
00801 
00802     try:
00803         with open(module_list) as f:
00804             mods = [l.split()[0] for l in f]
00805     except IOError:
00806         return []
00807 
00808     nonfree = []
00809     for m in mods:
00810         l = _get_module_license(m)
00811         if l and not ('GPL' in l or 'BSD' in l or 'MPL' in l or 'MIT' in l):
00812             nonfree.append(m)
00813 
00814     return nonfree
00815 
00816 
00817 def __drm_con_info(con):
00818     info = ''
00819     for f in os.listdir(con):
00820         path = os.path.join(con, f)
00821         if f == 'uevent' or not os.path.isfile(path):
00822             continue
00823         val = open(path).read().strip()
00824         # format some well-known attributes specially
00825         if f == 'modes':
00826             val = val.replace('\n', ' ')
00827         if f == 'edid':
00828             val = base64.b64encode(val)
00829             f += '-base64'
00830         info += '%s: %s\n' % (f, val)
00831     return info
00832 
00833 
00834 def attach_drm_info(report):
00835     '''Add information about DRM hardware.
00836 
00837     Collect information from /sys/class/drm/.
00838     '''
00839     drm_dir = '/sys/class/drm'
00840     if not os.path.isdir(drm_dir):
00841         return
00842     for f in os.listdir(drm_dir):
00843         con = os.path.join(drm_dir, f)
00844         if os.path.exists(os.path.join(con, 'enabled')):
00845             # DRM can set an arbitrary string for its connector paths.
00846             report['DRM.' + path_to_key(f)] = __drm_con_info(con)
00847 
00848 
00849 def in_session_of_problem(report):
00850     '''Check if the problem happened in the currently running XDG session.
00851 
00852     This can be used to determine if e. g. ~/.xsession-errors is relevant and
00853     should be attached.
00854 
00855     Return None if this cannot be determined due to not being able to talk to
00856     ConsoleKit.
00857     '''
00858     # report time is in local TZ
00859     orig_ctime = locale.getlocale(locale.LC_TIME)
00860     try:
00861         locale.setlocale(locale.LC_TIME, 'C')
00862         report_time = time.mktime(time.strptime(report['Date']))
00863     except KeyError:
00864         return None
00865     finally:
00866         locale.setlocale(locale.LC_TIME, orig_ctime)
00867 
00868     try:
00869         bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
00870         ck_manager = Gio.DBusProxy.new_sync(
00871             bus, Gio.DBusProxyFlags.NONE, None,
00872             'org.freedesktop.ConsoleKit', '/org/freedesktop/ConsoleKit/Manager',
00873             'org.freedesktop.ConsoleKit.Manager', None)
00874 
00875         cur_session = ck_manager.GetCurrentSession()
00876 
00877         ck_session = Gio.DBusProxy.new_sync(
00878             bus, Gio.DBusProxyFlags.NONE, None,
00879             'org.freedesktop.ConsoleKit', cur_session,
00880             'org.freedesktop.ConsoleKit.Session', None)
00881 
00882         session_start_time = ck_session.GetCreationTime()
00883     except GLib.GError as e:
00884         sys.stderr.write('Error connecting to ConsoleKit: %s\n' % str(e))
00885         return None
00886 
00887     m = re.match('(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d)(?:\.\d+Z)$', session_start_time)
00888     if m:
00889         # CK gives UTC time
00890         session_start_time = calendar.timegm(time.strptime(m.group(1), '%Y-%m-%dT%H:%M:%S'))
00891     else:
00892         sys.stderr.write('cannot parse time returned by CK: %s\n' % session_start_time)
00893         return None
00894 
00895     return session_start_time <= report_time
00896 
00897 
00898 def attach_default_grub(report, key=None):
00899     '''attach /etc/default/grub after filtering out password lines'''
00900 
00901     path = '/etc/default/grub'
00902     if not key:
00903         key = path_to_key(path)
00904 
00905     if os.path.exists(path):
00906         with open(path, 'r') as f:
00907             filtered = [l if not l.startswith('password')
00908                         else '### PASSWORD LINE REMOVED ###'
00909                         for l in f.readlines()]
00910             report[key] = ''.join(filtered)