Back to index

apport  2.3
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(report):
00265     '''Attach ALSA subsystem information to the report.
00266 
00267     (loosely based on http://www.alsa-project.org/alsa-info.sh)
00268     '''
00269     attach_file_if_exists(report, os.path.expanduser('~/.asoundrc'),
00270                           'UserAsoundrc')
00271     attach_file_if_exists(report, os.path.expanduser('~/.asoundrc.asoundconf'),
00272                           'UserAsoundrcAsoundconf')
00273     attach_file_if_exists(report, '/etc/asound.conf')
00274     attach_file_if_exists(report, '/proc/asound/version', 'AlsaVersion')
00275     attach_file(report, '/proc/cpuinfo', 'ProcCpuinfo')
00276 
00277     report['AlsaDevices'] = command_output(['ls', '-l', '/dev/snd/'])
00278     report['AplayDevices'] = command_output(['aplay', '-l'])
00279     report['ArecordDevices'] = command_output(['arecord', '-l'])
00280 
00281     report['PciMultimedia'] = pci_devices(PCI_MULTIMEDIA)
00282 
00283     cards = []
00284     if os.path.exists('/proc/asound/cards'):
00285         with open('/proc/asound/cards') as fd:
00286             for line in fd:
00287                 if ']:' in line:
00288                     fields = line.lstrip().split()
00289                     cards.append(int(fields[0]))
00290 
00291     for card in cards:
00292         key = 'Card%d.Amixer.info' % card
00293         report[key] = command_output(['amixer', '-c', str(card), 'info'])
00294         key = 'Card%d.Amixer.values' % card
00295         report[key] = command_output(['amixer', '-c', str(card)])
00296 
00297         for codecpath in glob.glob('/proc/asound/card%d/codec*' % card):
00298             if os.path.isfile(codecpath):
00299                 codec = os.path.basename(codecpath)
00300                 key = 'Card%d.Codecs.%s' % (card, path_to_key(codec))
00301                 attach_file(report, codecpath, key=key)
00302             elif os.path.isdir(codecpath):
00303                 codec = os.path.basename(codecpath)
00304                 for name in os.listdir(codecpath):
00305                     path = os.path.join(codecpath, name)
00306                     key = 'Card%d.Codecs.%s.%s' % (card, path_to_key(codec), path_to_key(name))
00307                     attach_file(report, path, key)
00308 
00309     report['AudioDevicesInUse'] = command_output(
00310         ['fuser', '-v'] + glob.glob('/dev/dsp*') + glob.glob('/dev/snd/*') + glob.glob('/dev/seq*'))
00311 
00312     if os.path.exists('/usr/bin/pacmd'):
00313         report['PulseList'] = command_output(['pacmd', 'list'])
00314 
00315     attach_dmi(report)
00316     attach_dmesg(report)
00317 
00318     # This seems redundant with the amixer info, do we need it?
00319     #report['AlsactlStore'] = command-output(['alsactl', '-f', '-', 'store'])
00320 
00321 
00322 def command_available(command):
00323     '''Is given command on the executable search path?'''
00324     if 'PATH' not in os.environ:
00325         return False
00326     path = os.environ['PATH']
00327     for element in path.split(os.pathsep):
00328         if not element:
00329             continue
00330         filename = os.path.join(element, command)
00331         if os.path.isfile(filename) and os.access(filename, os.X_OK):
00332             return True
00333     return False
00334 
00335 
00336 def command_output(command, input=None, stderr=subprocess.STDOUT,
00337                    keep_locale=False, decode_utf8=True):
00338     '''Try to execute given command (array) and return its stdout.
00339 
00340     In case of failure, a textual error gets returned. This function forces
00341     LC_MESSAGES to C, to avoid translated output in bug reports.
00342 
00343     If decode_utf8 is True (default), the output will be converted to a string,
00344     otherwise left as bytes.
00345     '''
00346     env = os.environ.copy()
00347     if not keep_locale:
00348         env['LC_MESSAGES'] = 'C'
00349     try:
00350         sp = subprocess.Popen(command, stdout=subprocess.PIPE,
00351                               stderr=stderr,
00352                               stdin=(input and subprocess.PIPE or None),
00353                               env=env)
00354     except OSError as e:
00355         return 'Error: ' + str(e)
00356 
00357     out = sp.communicate(input)[0]
00358     if sp.returncode == 0:
00359         res = out.strip()
00360     else:
00361         res = (b'Error: command ' + str(command).encode() + b' failed with exit code '
00362                + str(sp.returncode).encode() + b': ' + out)
00363 
00364     if decode_utf8:
00365         res = res.decode('UTF-8', errors='replace')
00366     return res
00367 
00368 
00369 def _root_command_prefix():
00370     if os.getuid() == 0:
00371         prefix = []
00372     elif os.getenv('DISPLAY') and \
00373             subprocess.call(['which', 'kdesudo'], stdout=subprocess.PIPE,
00374                             stderr=subprocess.PIPE) == 0 and \
00375             subprocess.call(['pgrep', '-x', '-u', str(os.getuid()), 'ksmserver'],
00376                             stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0:
00377         prefix = ['kdesudo', '--desktop', '/usr/share/applications/apport-kde-mime.desktop',
00378                   '--', 'env', '-u', 'LANGUAGE', 'LC_MESSAGES=C']
00379     elif os.getenv('DISPLAY') and \
00380             subprocess.call(['which', 'gksu'], stdout=subprocess.PIPE,
00381                             stderr=subprocess.PIPE) == 0 and \
00382             subprocess.call(['pgrep', '-x', '-u', str(os.getuid()), 'gnome-panel|gconfd-2'],
00383                             stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0:
00384         prefix = ['gksu', '-D', 'Apport', '--', 'env', '-u', 'LANGUAGE', 'LC_MESSAGES=C']
00385     else:
00386         prefix = ['sudo', 'LC_MESSAGES=C', 'LANGUAGE=']
00387 
00388     return prefix
00389 
00390 
00391 def root_command_output(command, input=None, stderr=subprocess.STDOUT, decode_utf8=True):
00392     '''Try to execute given command (array) as root and return its stdout.
00393 
00394     This passes the command through gksu, kdesudo, or sudo, depending on the
00395     running desktop environment.
00396 
00397     In case of failure, a textual error gets returned.
00398 
00399     If decode_utf8 is True (default), the output will be converted to a string,
00400     otherwise left as bytes.
00401     '''
00402     assert isinstance(command, list), 'command must be a list'
00403     return command_output(_root_command_prefix() + command, input, stderr,
00404                           keep_locale=True, decode_utf8=decode_utf8)
00405 
00406 
00407 def attach_root_command_outputs(report, command_map):
00408     '''Execute multiple commands as root and put their outputs into report.
00409 
00410     command_map is a keyname -> 'shell command' dictionary with the commands to
00411     run. They are all run through /bin/sh, so you need to take care of shell
00412     escaping yourself. To include stderr output of a command, end it with
00413     "2>&1".
00414 
00415     Just like root_command_output() this will use gksu, kdesudo, or sudo for
00416     gaining root privileges, depending on the running desktop environment.
00417 
00418     This is preferrable to using root_command_output() multiple times, as that
00419     will ask for the password every time.
00420     '''
00421     workdir = tempfile.mkdtemp()
00422     try:
00423         # create a shell script with all the commands
00424         script_path = os.path.join(workdir, ':script:')
00425         script = open(script_path, 'w')
00426         for keyname, command in command_map.items():
00427             assert hasattr(command, 'strip'), 'command must be a string (shell command)'
00428             # use "| cat" here, so that we can end commands with 2>&1
00429             # (otherwise it would have the wrong redirection order)
00430             script.write('%s | cat > %s\n' % (command, os.path.join(workdir, keyname)))
00431         script.close()
00432 
00433         # run script
00434         sp = subprocess.Popen(_root_command_prefix() + ['/bin/sh', script_path])
00435         sp.wait()
00436 
00437         # now read back the individual outputs
00438         for keyname in command_map:
00439             f = open(os.path.join(workdir, keyname))
00440             buf = f.read().strip()
00441             if buf:
00442                 report[keyname] = buf
00443             f.close()
00444     finally:
00445         shutil.rmtree(workdir)
00446 
00447 
00448 def recent_syslog(pattern):
00449     '''Extract recent messages from syslog which match a regex.
00450 
00451     pattern should be a "re" object.
00452     '''
00453     return recent_logfile('/var/log/syslog', pattern)
00454 
00455 
00456 def recent_logfile(logfile, pattern, maxlines=10000):
00457     '''Extract recent messages from a logfile which match a regex.
00458 
00459     pattern should be a "re" object. By default this catches at most the last
00460     1000 lines, but this can be modified with a different maxlines argument.
00461     '''
00462     lines = ''
00463     try:
00464         tail = subprocess.Popen(['tail', '-n', str(maxlines), logfile],
00465                                 stdout=subprocess.PIPE)
00466         while tail.poll() is None:
00467             for line in tail.stdout:
00468                 line = line.decode('UTF-8', errors='replace')
00469                 if pattern.search(line):
00470                     lines += line
00471         tail.stdout.close()
00472         tail.wait()
00473     except IOError:
00474         return ''
00475     return lines
00476 
00477 
00478 def xsession_errors(pattern=None):
00479     '''Extract messages from ~/.xsession-errors.
00480 
00481     By default this parses out glib-style warnings, errors, criticals etc. and
00482     X window errors.  You can specify a "re" object as pattern to customize the
00483     filtering.
00484 
00485     Please note that you should avoid attaching the whole file to reports, as
00486     it can, and often does, contain sensitive and private data.
00487     '''
00488     path = os.path.expanduser('~/.xsession-errors')
00489     if not os.path.exists(path):
00490         return ''
00491 
00492     if not pattern:
00493         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)')
00494 
00495     lines = ''
00496     with open(path, 'rb') as f:
00497         for line in f:
00498             line = line.decode('UTF-8', errors='replace')
00499             if pattern.search(line):
00500                 lines += line
00501     return lines
00502 
00503 PCI_MASS_STORAGE = 0x01
00504 PCI_NETWORK = 0x02
00505 PCI_DISPLAY = 0x03
00506 PCI_MULTIMEDIA = 0x04
00507 PCI_MEMORY = 0x05
00508 PCI_BRIDGE = 0x06
00509 PCI_SIMPLE_COMMUNICATIONS = 0x07
00510 PCI_BASE_SYSTEM_PERIPHERALS = 0x08
00511 PCI_INPUT_DEVICES = 0x09
00512 PCI_DOCKING_STATIONS = 0x0a
00513 PCI_PROCESSORS = 0x0b
00514 PCI_SERIAL_BUS = 0x0c
00515 
00516 
00517 def pci_devices(*pci_classes):
00518     '''Return a text dump of PCI devices attached to the system.'''
00519 
00520     if not pci_classes:
00521         return command_output(['lspci', '-vvnn'])
00522 
00523     result = ''
00524     output = command_output(['lspci', '-vvmmnn'])
00525     for paragraph in output.split('\n\n'):
00526         pci_class = None
00527         slot = None
00528 
00529         for line in paragraph.split('\n'):
00530             try:
00531                 key, value = line.split(':', 1)
00532             except ValueError:
00533                 continue
00534             value = value.strip()
00535             key = key.strip()
00536             if key == 'Class':
00537                 n = int(value[-5:-1], 16)
00538                 pci_class = (n & 0xff00) >> 8
00539             elif key == 'Slot':
00540                 slot = value
00541 
00542         if pci_class and slot and pci_class in pci_classes:
00543             if result:
00544                 result += '\n\n'
00545             result += command_output(['lspci', '-vvnns', slot]).strip()
00546 
00547     return result
00548 
00549 
00550 def usb_devices():
00551     '''Return a text dump of USB devices attached to the system.'''
00552 
00553     # TODO: would be nice to be able to filter by interface class
00554     return command_output(['lsusb', '-v'])
00555 
00556 
00557 def files_in_package(package, globpat=None):
00558     '''Retrieve a list of files owned by package, optionally matching globpat'''
00559 
00560     files = packaging.get_files(package)
00561     if globpat:
00562         result = [f for f in files if glob.fnmatch.fnmatch(f, globpat)]
00563     else:
00564         result = files
00565     return result
00566 
00567 
00568 def attach_gconf(report, package):
00569     '''Obsolete'''
00570 
00571     # keeping a no-op function for some time to not break hooks
00572     pass
00573 
00574 
00575 def attach_gsettings_schema(report, schema):
00576     '''Attach user-modified gsttings keys of a schema.'''
00577 
00578     cur_value = report.get('GsettingsChanges', '')
00579 
00580     defaults = {}  # schema -> key ->  value
00581     env = os.environ.copy()
00582     env['XDG_CONFIG_HOME'] = '/nonexisting'
00583     gsettings = subprocess.Popen(['gsettings', 'list-recursively', schema],
00584                                  env=env, stdout=subprocess.PIPE)
00585     for l in gsettings.stdout:
00586         try:
00587             (schema, key, value) = l.split(None, 2)
00588             value = value.rstrip()
00589         except ValueError:
00590             continue  # invalid line
00591         defaults.setdefault(schema, {})[key] = value
00592 
00593     gsettings = subprocess.Popen(['gsettings', 'list-recursively', schema],
00594                                  stdout=subprocess.PIPE)
00595     for l in gsettings.stdout:
00596         try:
00597             (schema, key, value) = l.split(None, 2)
00598             value = value.rstrip()
00599         except ValueError:
00600             continue  # invalid line
00601 
00602         if value != defaults.get(schema, {}).get(key, ''):
00603             cur_value += '%s %s %s\n' % (schema, key, value)
00604 
00605     report['GsettingsChanges'] = cur_value
00606 
00607 
00608 def attach_gsettings_package(report, package):
00609     '''Attach user-modified gsettings keys of all schemas in a package.'''
00610 
00611     for schema_file in files_in_package(package, '/usr/share/glib-2.0/schemas/*.gschema.xml'):
00612         schema = os.path.basename(schema_file)[:-12]
00613         attach_gsettings_schema(report, schema)
00614 
00615 
00616 def attach_network(report):
00617     '''Attach generic network-related information to report.'''
00618 
00619     report['IpRoute'] = command_output(['ip', 'route'])
00620     report['IpAddr'] = command_output(['ip', 'addr'])
00621     report['PciNetwork'] = pci_devices(PCI_NETWORK)
00622     attach_file_if_exists(report, '/etc/network/interfaces', key='IfupdownConfig')
00623 
00624     for var in ('http_proxy', 'ftp_proxy', 'no_proxy'):
00625         if var in os.environ:
00626             report[var] = os.environ[var]
00627 
00628 
00629 def attach_wifi(report):
00630     '''Attach wireless (WiFi) network information to report.'''
00631 
00632     report['WifiSyslog'] = recent_syslog(re.compile(r'(NetworkManager|modem-manager|dhclient|kernel|wpa_supplicant)(\[\d+\])?:'))
00633     report['IwConfig'] = re.sub(
00634         'ESSID:(.*)', 'ESSID:<hidden>',
00635         re.sub('Encryption key:(.*)', 'Encryption key: <hidden>',
00636                re.sub('Access Point: (.*)', 'Access Point: <hidden>',
00637                       command_output(['iwconfig']))))
00638     report['RfKill'] = command_output(['rfkill', 'list'])
00639     report['CRDA'] = command_output(['iw', 'reg', 'get'])
00640 
00641     attach_file_if_exists(report, '/var/log/wpa_supplicant.log', key='WpaSupplicantLog')
00642 
00643 
00644 def attach_printing(report):
00645     '''Attach printing information to the report.
00646 
00647     Based on http://wiki.ubuntu.com/PrintingBugInfoScript.
00648     '''
00649     attach_file_if_exists(report, '/etc/papersize', 'Papersize')
00650     attach_file_if_exists(report, '/var/log/cups/error_log', 'CupsErrorLog')
00651     report['Locale'] = command_output(['locale'])
00652     report['Lpstat'] = command_output(['lpstat', '-v'])
00653 
00654     ppds = glob.glob('/etc/cups/ppd/*.ppd')
00655     if ppds:
00656         nicknames = command_output(['fgrep', '-H', '*NickName'] + ppds)
00657         report['PpdFiles'] = re.sub('/etc/cups/ppd/(.*).ppd:\*NickName: *"(.*)"', '\g<1>: \g<2>', nicknames)
00658 
00659     report['PrintingPackages'] = package_versions(
00660         'foo2zjs', 'foomatic-db', 'foomatic-db-engine',
00661         'foomatic-db-gutenprint', 'foomatic-db-hpijs', 'foomatic-filters',
00662         'foomatic-gui', 'hpijs', 'hplip', 'm2300w', 'min12xxw', 'c2050',
00663         'hpoj', 'pxljr', 'pnm2ppa', 'splix', 'hp-ppd', 'hpijs-ppds',
00664         'linuxprinting.org-ppds', 'openprinting-ppds',
00665         'openprinting-ppds-extra', 'ghostscript', 'cups',
00666         'cups-driver-gutenprint', 'foomatic-db-gutenprint', 'ijsgutenprint',
00667         'cupsys-driver-gutenprint', 'gimp-gutenprint', 'gutenprint-doc',
00668         'gutenprint-locales', 'system-config-printer-common', 'kdeprint')
00669 
00670 
00671 def attach_mac_events(report):
00672     '''Attach MAC information and events to the report.'''
00673 
00674     mac_regex = 'audit\(|apparmor|selinux|security'
00675     mac_re = re.compile(mac_regex, re.IGNORECASE)
00676     aa_denied_regex = 'apparmor="DENIED"'
00677     aa_denied_re = re.compile(aa_denied_regex, re.IGNORECASE)
00678 
00679     if os.path.exists('/var/log/kern.log'):
00680         report['KernLog'] = recent_logfile('/var/log/kern.log', mac_re)
00681     elif os.path.exists('/var/log/messages'):
00682         report['KernLog'] = recent_logfile('/var/log/messages', mac_re)
00683 
00684     if os.path.exists('/var/run/auditd.pid'):
00685         attach_root_command_outputs(report, {'AuditLog': 'egrep "' + mac_regex + '" /var/log/audit/audit.log'})
00686 
00687     attach_file(report, '/proc/version_signature', 'ProcVersionSignature')
00688     attach_file(report, '/proc/cmdline', 'ProcCmdline')
00689 
00690     if re.search(aa_denied_re, report.get('KernLog', '')) or re.search(aa_denied_re, report.get('AuditLog', '')):
00691         tags = report.get('Tags', '')
00692         if tags:
00693             tags += ' '
00694         report['Tags'] = tags + 'apparmor'
00695 
00696 
00697 def attach_related_packages(report, packages):
00698     '''Attach version information for related packages
00699 
00700     In the future, this might also run their hooks.
00701     '''
00702     report['RelatedPackageVersions'] = package_versions(*packages)
00703 
00704 
00705 def package_versions(*packages):
00706     '''Return a text listing of package names and versions.
00707 
00708     Arguments may be package names or globs, e. g. "foo*"
00709     '''
00710     versions = []
00711     for package_pattern in packages:
00712         if not package_pattern:
00713             continue
00714 
00715         matching_packages = packaging.package_name_glob(package_pattern)
00716 
00717         if not matching_packages:
00718             versions.append((package_pattern, 'N/A'))
00719 
00720         for package in sorted(matching_packages):
00721             try:
00722                 version = packaging.get_version(package)
00723             except ValueError:
00724                 version = 'N/A'
00725             if version is None:
00726                 version = 'N/A'
00727             versions.append((package, version))
00728 
00729     package_width, version_width = \
00730         map(max, [map(len, t) for t in zip(*versions)])
00731 
00732     fmt = '%%-%ds %%s' % package_width
00733     return '\n'.join([fmt % v for v in versions])
00734 
00735 
00736 def shared_libraries(path):
00737     '''Returns a list of strings containing the sonames of shared libraries
00738     with which the specified binary is linked.'''
00739 
00740     libs = set()
00741 
00742     for line in command_output(['ldd', path]).split('\n'):
00743         try:
00744             lib, rest = line.split('=>', 1)
00745         except ValueError:
00746             continue
00747 
00748         lib = lib.strip()
00749         libs.add(lib)
00750 
00751     return libs
00752 
00753 
00754 def links_with_shared_library(path, lib):
00755     '''Returns True if the binary at path links with the library named lib.
00756 
00757     path should be a fully qualified path (e.g. report['ExecutablePath'])
00758     lib may be of the form 'lib<name>' or 'lib<name>.so.<version>'
00759     '''
00760 
00761     libs = shared_libraries(path)
00762 
00763     if lib in libs:
00764         return True
00765 
00766     for linked_lib in libs:
00767         if linked_lib.startswith(lib + '.so.'):
00768             return True
00769 
00770     return False
00771 
00772 
00773 def _get_module_license(module):
00774     '''Return the license for a given kernel module.'''
00775 
00776     try:
00777         modinfo = subprocess.Popen(['/sbin/modinfo', module],
00778                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)
00779         out = modinfo.communicate()[0].decode('UTF-8')
00780         if modinfo.returncode != 0:
00781             return 'invalid'
00782     except OSError:
00783         return None
00784     for l in out.splitlines():
00785         fields = l.split(':', 1)
00786         if len(fields) < 2:
00787             continue
00788         if fields[0] == 'license':
00789             return fields[1].strip()
00790 
00791     return None
00792 
00793 
00794 def nonfree_kernel_modules(module_list='/proc/modules'):
00795     '''Check loaded modules and return a list of those which are not free.'''
00796 
00797     try:
00798         with open(module_list) as f:
00799             mods = [l.split()[0] for l in f]
00800     except IOError:
00801         return []
00802 
00803     nonfree = []
00804     for m in mods:
00805         l = _get_module_license(m)
00806         if l and not ('GPL' in l or 'BSD' in l or 'MPL' in l or 'MIT' in l):
00807             nonfree.append(m)
00808 
00809     return nonfree
00810 
00811 
00812 def __drm_con_info(con):
00813     info = ''
00814     for f in os.listdir(con):
00815         path = os.path.join(con, f)
00816         if f == 'uevent' or not os.path.isfile(path):
00817             continue
00818         val = open(path).read().strip()
00819         # format some well-known attributes specially
00820         if f == 'modes':
00821             val = val.replace('\n', ' ')
00822         if f == 'edid':
00823             val = base64.b64encode(val)
00824             f += '-base64'
00825         info += '%s: %s\n' % (f, val)
00826     return info
00827 
00828 
00829 def attach_drm_info(report):
00830     '''Add information about DRM hardware.
00831 
00832     Collect information from /sys/class/drm/.
00833     '''
00834     drm_dir = '/sys/class/drm'
00835     if not os.path.isdir(drm_dir):
00836         return
00837     for f in os.listdir(drm_dir):
00838         con = os.path.join(drm_dir, f)
00839         if os.path.exists(os.path.join(con, 'enabled')):
00840             # DRM can set an arbitrary string for its connector paths.
00841             report['DRM.' + path_to_key(f)] = __drm_con_info(con)
00842 
00843 
00844 def in_session_of_problem(report):
00845     '''Check if the problem happened in the currently running XDG session.
00846 
00847     This can be used to determine if e. g. ~/.xsession-errors is relevant and
00848     should be attached.
00849 
00850     Return None if this cannot be determined due to not being able to talk to
00851     ConsoleKit.
00852     '''
00853     # report time is in local TZ
00854     orig_ctime = locale.getlocale(locale.LC_TIME)
00855     try:
00856         locale.setlocale(locale.LC_TIME, 'C')
00857         report_time = time.mktime(time.strptime(report['Date']))
00858     except KeyError:
00859         return None
00860     finally:
00861         locale.setlocale(locale.LC_TIME, orig_ctime)
00862 
00863     try:
00864         bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
00865         ck_manager = Gio.DBusProxy.new_sync(
00866             bus, Gio.DBusProxyFlags.NONE, None,
00867             'org.freedesktop.ConsoleKit', '/org/freedesktop/ConsoleKit/Manager',
00868             'org.freedesktop.ConsoleKit.Manager', None)
00869 
00870         cur_session = ck_manager.GetCurrentSession()
00871 
00872         ck_session = Gio.DBusProxy.new_sync(
00873             bus, Gio.DBusProxyFlags.NONE, None,
00874             'org.freedesktop.ConsoleKit', cur_session,
00875             'org.freedesktop.ConsoleKit.Session', None)
00876 
00877         session_start_time = ck_session.GetCreationTime()
00878     except GLib.GError as e:
00879         sys.stderr.write('Error connecting to ConsoleKit: %s\n' % str(e))
00880         return None
00881 
00882     m = re.match('(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d)(?:\.\d+Z)$', session_start_time)
00883     if m:
00884         # CK gives UTC time
00885         session_start_time = calendar.timegm(time.strptime(m.group(1), '%Y-%m-%dT%H:%M:%S'))
00886     else:
00887         sys.stderr.write('cannot parse time returned by CK: %s\n' % session_start_time)
00888         return None
00889 
00890     return session_start_time <= report_time
00891 
00892 
00893 def attach_default_grub(report, key=None):
00894     '''attach /etc/default/grub after filtering out password lines'''
00895 
00896     path = '/etc/default/grub'
00897     if not key:
00898         key = path_to_key(path)
00899 
00900     if os.path.exists(path):
00901         with open(path, 'r') as f:
00902             filtered = [l if not l.startswith('password')
00903                         else '### PASSWORD LINE REMOVED ###'
00904                         for l in f.readlines()]
00905             report[key] = ''.join(filtered)