Back to index

apport  2.3
Public Member Functions | Public Attributes | Private Member Functions | Private Attributes
apport.crashdb_impl.launchpad.CrashDatabase Class Reference
Inheritance diagram for apport.crashdb_impl.launchpad.CrashDatabase:
Inheritance graph
[legend]
Collaboration diagram for apport.crashdb_impl.launchpad.CrashDatabase:
Collaboration graph
[legend]

List of all members.

Public Member Functions

def __init__
def launchpad
def lp_distro
def upload
def get_hostname
def get_comment_url
def get_id_url
def download
def update
def update_traces
def get_distro_release
def get_affected_packages
def is_reporter
def can_update
def get_unretraced
def get_dup_unchecked
def get_unfixed
def get_fixed_version
def duplicate_of
def close_duplicate
def mark_regression
def mark_retraced
def mark_retrace_failed
def known
def get_bugpattern_baseurl
def accepts
def init_duplicate_db
def check_duplicate
def duplicate_db_fixed
def duplicate_db_remove
def duplicate_db_change_master_id
def duplicate_db_publish
def duplicate_sig_hash
def set_credentials

Public Attributes

 distro
 arch_tag
 options
 auth
 auth_file
 duplicate_db
 format_version

Private Member Functions

def _get_distro_tasks
def _get_source_version
def _mark_dup_checked
def _subscribe_triaging_team
def _generate_upload_blob

Private Attributes

 __launchpad
 __lp_distro
 __lpcache

Detailed Description

Launchpad implementation of crash database interface.

Definition at line 58 of file launchpad.py.


Constructor & Destructor Documentation

def apport.crashdb_impl.launchpad.CrashDatabase.__init__ (   self,
  auth,
  options 
)
Initialize Launchpad crash database.

You need to specify a launchpadlib-style credentials file to
access launchpad. If you supply None, it will use
default_credentials_path (~/.cache/apport/launchpad.credentials).

Recognized options are:
- distro: Name of the distribution in Launchpad
- project: Name of the project in Launchpad
(Note that exactly one of "distro" or "project" must be given.)
- launchpad_instance: If set, this uses the given launchpad instance
  instead of production (optional). This can be overriden or set by
  $APPORT_LAUNCHPAD_INSTANCE environment.
- cache_dir: Path to a permanent cache directory; by default it uses a
  temporary one. (optional). This can be overridden or set by
  $APPORT_LAUNCHPAD_CACHE environment.
- escalation_subscription: This subscribes the given person or team to
  a bug once it gets the 10th duplicate.
- escalation_tag: This adds the given tag to a bug once it gets more
  than 10 duplicates.
- initial_subscriber: The Launchpad user which gets subscribed to newly
  filed bugs (default: "apport"). It should be a bot user which the
  crash-digger instance runs as, as this will get to see all bug
  details immediately.
- triaging_team: The Launchpad user/team which gets subscribed after
  updating a crash report bug by the retracer (default:
  "ubuntu-crashes-universe")

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 61 of file launchpad.py.

00061 
00062     def __init__(self, auth, options):
00063         '''Initialize Launchpad crash database.
00064 
00065         You need to specify a launchpadlib-style credentials file to
00066         access launchpad. If you supply None, it will use
00067         default_credentials_path (~/.cache/apport/launchpad.credentials).
00068 
00069         Recognized options are:
00070         - distro: Name of the distribution in Launchpad
00071         - project: Name of the project in Launchpad
00072         (Note that exactly one of "distro" or "project" must be given.)
00073         - launchpad_instance: If set, this uses the given launchpad instance
00074           instead of production (optional). This can be overriden or set by
00075           $APPORT_LAUNCHPAD_INSTANCE environment.
00076         - cache_dir: Path to a permanent cache directory; by default it uses a
00077           temporary one. (optional). This can be overridden or set by
00078           $APPORT_LAUNCHPAD_CACHE environment.
00079         - escalation_subscription: This subscribes the given person or team to
00080           a bug once it gets the 10th duplicate.
00081         - escalation_tag: This adds the given tag to a bug once it gets more
00082           than 10 duplicates.
00083         - initial_subscriber: The Launchpad user which gets subscribed to newly
00084           filed bugs (default: "apport"). It should be a bot user which the
00085           crash-digger instance runs as, as this will get to see all bug
00086           details immediately.
00087         - triaging_team: The Launchpad user/team which gets subscribed after
00088           updating a crash report bug by the retracer (default:
00089           "ubuntu-crashes-universe")
00090         '''
00091         if os.getenv('APPORT_LAUNCHPAD_INSTANCE'):
00092             options['launchpad_instance'] = os.getenv(
00093                 'APPORT_LAUNCHPAD_INSTANCE')
00094         if not auth:
00095             lp_instance = options.get('launchpad_instance')
00096             if lp_instance:
00097                 auth = default_credentials_path + '.' + lp_instance.split('://', 1)[-1]
00098             else:
00099                 auth = default_credentials_path
00100         apport.crashdb.CrashDatabase.__init__(self, auth, options)
00101 
00102         self.distro = options.get('distro')
00103         if self.distro:
00104             assert 'project' not in options, 'Must not set both "project" and "distro" option'
00105         else:
00106             assert 'project' in options, 'Need to have either "project" or "distro" option'
00107 
00108         self.arch_tag = 'need-%s-retrace' % apport.packaging.get_system_architecture()
00109         self.options = options
00110         self.auth = auth
00111         assert self.auth
00112 
00113         self.__launchpad = None
00114         self.__lp_distro = None
00115         self.__lpcache = os.getenv('APPORT_LAUNCHPAD_CACHE', options.get('cache_dir'))

Here is the call graph for this function:


Member Function Documentation

Generate a multipart/MIME temporary file for uploading.

You have to close the returned file object after you are done with it.

Definition at line 898 of file launchpad.py.

00898 
00899     def _generate_upload_blob(self, report):
00900         '''Generate a multipart/MIME temporary file for uploading.
00901 
00902         You have to close the returned file object after you are done with it.
00903         '''
00904         # set reprocessing tags
00905         hdr = {}
00906         hdr['Tags'] = 'apport-%s' % report['ProblemType'].lower()
00907         a = report.get('PackageArchitecture')
00908         if not a or a == 'all':
00909             a = report.get('Architecture')
00910         if a:
00911             hdr['Tags'] += ' ' + a
00912         if 'Tags' in report:
00913             hdr['Tags'] += ' ' + report['Tags'].lower()
00914 
00915         # privacy/retracing for distro reports
00916         # FIXME: ugly hack until LP has a real crash db
00917         if 'DistroRelease' in report:
00918             if a and ('VmCore' in report or 'CoreDump' in report):
00919                 hdr['Private'] = 'yes'
00920                 hdr['Subscribers'] = self.options.get('initial_subscriber', 'apport')
00921                 hdr['Tags'] += ' need-%s-retrace' % a
00922             elif 'Traceback' in report:
00923                 hdr['Private'] = 'yes'
00924                 hdr['Subscribers'] = 'apport'
00925                 hdr['Tags'] += ' need-duplicate-check'
00926         if 'DuplicateSignature' in report and 'need-duplicate-check' not in hdr['Tags']:
00927                 hdr['Tags'] += ' need-duplicate-check'
00928 
00929         # if we have checkbox submission key, link it to the bug; keep text
00930         # reference until the link is shown in Launchpad's UI
00931         if 'CheckboxSubmission' in report:
00932             hdr['HWDB-Submission'] = report['CheckboxSubmission']
00933 
00934         # order in which keys should appear in the temporary file
00935         order = ['ProblemType', 'DistroRelease', 'Package', 'Regression', 'Reproducible',
00936                  'TestedUpstream', 'ProcVersionSignature', 'Uname', 'NonfreeKernelModules']
00937 
00938         # write MIME/Multipart version into temporary file
00939         mime = tempfile.TemporaryFile()
00940         report.write_mime(mime, extra_headers=hdr, skip_keys=['Tags'], priority_fields=order)
00941         mime.flush()
00942         mime.seek(0)
00943 
00944         return mime
00945 
00946 #
00947 # Launchpad storeblob API (should go into launchpadlib, see LP #315358)
00948 #

Here is the caller graph for this function:

Definition at line 153 of file launchpad.py.

00153 
00154     def _get_distro_tasks(self, tasks):
00155         if not self.distro:
00156             raise StopIteration
00157 
00158         for t in tasks:
00159             if t.bug_target_name.lower() == self.distro or \
00160                     re.match('^.+\(%s.*\)$' % self.distro, t.bug_target_name.lower()):
00161                 yield t

Here is the caller graph for this function:

def apport.crashdb_impl.launchpad.CrashDatabase._get_source_version (   self,
  package 
) [private]
Return the version of given source package in the latest release of
given distribution.

If 'distro' is None, we will look for a launchpad project .

Definition at line 541 of file launchpad.py.

00541 
00542     def _get_source_version(self, package):
00543         '''Return the version of given source package in the latest release of
00544         given distribution.
00545 
00546         If 'distro' is None, we will look for a launchpad project .
00547         '''
00548         sources = self.lp_distro.main_archive.getPublishedSources(
00549             exact_match=True,
00550             source_name=package,
00551             distro_series=self.lp_distro.current_series
00552         )
00553         # first element is the latest one
00554         return sources[0].source_package_version

Here is the caller graph for this function:

def apport.crashdb_impl.launchpad.CrashDatabase._mark_dup_checked (   self,
  id,
  report 
) [private]
Mark crash id as checked for being a duplicate.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 801 of file launchpad.py.

00801 
00802     def _mark_dup_checked(self, id, report):
00803         '''Mark crash id as checked for being a duplicate.'''
00804 
00805         bug = self.launchpad.bugs[id]
00806 
00807         # if we have a distro task without a package, fix it
00808         if 'SourcePackage' in report:
00809             for task in bug.bug_tasks:
00810                 if task.target.resource_type_link.endswith('#distribution'):
00811                     task.target = self.lp_distro.getSourcePackage(
00812                         name=report['SourcePackage'])
00813                     task.lp_save()
00814                     bug = self.launchpad.bugs[id]
00815                     break
00816 
00817         if 'need-duplicate-check' in bug.tags:
00818             x = bug.tags[:]  # LP#254901 workaround
00819             x.remove('need-duplicate-check')
00820             bug.tags = x
00821             bug.lp_save()
00822             if 'Traceback' in report:
00823                 for task in bug.bug_tasks:
00824                     if task.target.resource_type_link.endswith('#distribution'):
00825                         if task.importance == 'Undecided':
00826                             task.importance = 'Medium'
00827                             task.lp_save()
00828         self._subscribe_triaging_team(bug, report)

Here is the call graph for this function:

def apport.crashdb_impl.launchpad.CrashDatabase._subscribe_triaging_team (   self,
  bug,
  report 
) [private]
Subscribe the right triaging team to the bug.

Definition at line 884 of file launchpad.py.

00884 
00885     def _subscribe_triaging_team(self, bug, report):
00886         '''Subscribe the right triaging team to the bug.'''
00887 
00888         #FIXME: this entire function is an ugly Ubuntu specific hack until LP
00889         #gets a real crash db; see https://wiki.ubuntu.com/CrashReporting
00890 
00891         if 'DistroRelease' in report and report['DistroRelease'].split()[0] != 'Ubuntu':
00892             return  # only Ubuntu bugs are filed private
00893 
00894         #use a url hack here, it is faster
00895         person = '%s~%s' % (self.launchpad._root_uri,
00896                             self.options.get('triaging_team', 'ubuntu-crashes-universe'))
00897         bug.subscribe(person=person)

Here is the caller graph for this function:

def apport.crashdb.CrashDatabase.accepts (   self,
  report 
) [inherited]
Check if this report can be uploaded to this database.

Crash databases might limit the types of reports they get with e. g.
the "problem_types" option.

Definition at line 60 of file crashdb.py.

00060 
00061     def accepts(self, report):
00062         '''Check if this report can be uploaded to this database.
00063 
00064         Crash databases might limit the types of reports they get with e. g.
00065         the "problem_types" option.
00066         '''
00067         if 'problem_types' in self.options:
00068             return report.get('ProblemType') in self.options['problem_types']
00069 
00070         return True

Here is the caller graph for this function:

Check whether the user is eligible to update a report.

A user should add additional information to an existing ID if (s)he is
the reporter or subscribed, the bug is open, not a duplicate, etc. The
exact policy and checks should be done according to  the particular
implementation.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 481 of file launchpad.py.

00481 
00482     def can_update(self, id):
00483         '''Check whether the user is eligible to update a report.
00484 
00485         A user should add additional information to an existing ID if (s)he is
00486         the reporter or subscribed, the bug is open, not a duplicate, etc. The
00487         exact policy and checks should be done according to  the particular
00488         implementation.
00489         '''
00490         bug = self.launchpad.bugs[id]
00491         if bug.duplicate_of:
00492             return False
00493 
00494         if bug.owner.name == self.launchpad.me.name:
00495             return True
00496 
00497         # check subscription
00498         me = self.launchpad.me.self_link
00499         for sub in bug.subscriptions.entries:
00500             if sub['person_link'] == me:
00501                 return True
00502 
00503         return False

def apport.crashdb.CrashDatabase.check_duplicate (   self,
  id,
  report = None 
) [inherited]
Check whether a crash is already known.

If the crash is new, it will be added to the duplicate database and the
function returns None. If the crash is already known, the function
returns a pair (crash_id, fixed_version), where fixed_version might be
None if the crash is not fixed in the latest version yet. Depending on
whether the version in report is smaller than/equal to the fixed
version or larger, this calls close_duplicate() or mark_regression().

If the report does not have a valid crash signature, this function does
nothing and just returns None.

By default, the report gets download()ed, but for performance reasons
it can be explicitly passed to this function if it is already available.

Definition at line 133 of file crashdb.py.

00133 
00134     def check_duplicate(self, id, report=None):
00135         '''Check whether a crash is already known.
00136 
00137         If the crash is new, it will be added to the duplicate database and the
00138         function returns None. If the crash is already known, the function
00139         returns a pair (crash_id, fixed_version), where fixed_version might be
00140         None if the crash is not fixed in the latest version yet. Depending on
00141         whether the version in report is smaller than/equal to the fixed
00142         version or larger, this calls close_duplicate() or mark_regression().
00143 
00144         If the report does not have a valid crash signature, this function does
00145         nothing and just returns None.
00146 
00147         By default, the report gets download()ed, but for performance reasons
00148         it can be explicitly passed to this function if it is already available.
00149         '''
00150         assert self.duplicate_db, 'init_duplicate_db() needs to be called before'
00151 
00152         if not report:
00153             report = self.download(id)
00154 
00155         self._mark_dup_checked(id, report)
00156 
00157         if 'DuplicateSignature' in report:
00158             sig = report['DuplicateSignature']
00159         else:
00160             sig = report.crash_signature()
00161         existing = []
00162         if sig:
00163             # use real duplicate signature
00164             existing = self._duplicate_search_signature(sig, id)
00165 
00166             if existing:
00167                 # update status of existing master bugs
00168                 for (ex_id, _) in existing:
00169                     self._duplicate_db_sync_status(ex_id)
00170                 existing = self._duplicate_search_signature(sig, id)
00171 
00172         try:
00173             report_package_version = report['Package'].split()[1]
00174         except (KeyError, IndexError):
00175             report_package_version = None
00176 
00177         # check the existing IDs whether there is one that is unfixed or not
00178         # older than the report's package version; if so, we have a duplicate.
00179         master_id = None
00180         master_ver = None
00181         for (ex_id, ex_ver) in existing:
00182             if not ex_ver or not report_package_version or apport.packaging.compare_versions(report_package_version, ex_ver) < 0:
00183                 master_id = ex_id
00184                 master_ver = ex_ver
00185                 break
00186         else:
00187             # if we did not find a new enough open master report,
00188             # we have a regression of the latest fix. Mark it so, and create a
00189             # new unfixed ID for it later on
00190             if existing:
00191                 self.mark_regression(id, existing[-1][0])
00192 
00193         # now query address signatures, they might turn up another duplicate
00194         # (not necessarily the same, due to Stacktraces sometimes being
00195         # slightly different)
00196         addr_sig = report.crash_signature_addresses()
00197         if addr_sig:
00198             addr_match = self._duplicate_search_address_signature(addr_sig)
00199             if addr_match and addr_match != master_id:
00200                 if master_id is None:
00201                     # we have a duplicate only identified by address sig, close it
00202                     master_id = addr_match
00203                 else:
00204                     # our bug is a dupe of two different masters, one from
00205                     # symbolic, the other from addr matching (see LP#943117);
00206                     # make them all duplicates of each other, using the lower
00207                     # number as master
00208                     if master_id < addr_match:
00209                         self.close_duplicate(report, addr_match, master_id)
00210                         self._duplicate_db_merge_id(addr_match, master_id)
00211                     else:
00212                         self.close_duplicate(report, master_id, addr_match)
00213                         self._duplicate_db_merge_id(master_id, addr_match)
00214                         master_id = addr_match
00215                         master_ver = None  # no version tracking for address signatures yet
00216 
00217         if master_id is not None:
00218             if addr_sig:
00219                 self._duplicate_db_add_address_signature(addr_sig, master_id)
00220             self.close_duplicate(report, id, master_id)
00221             return (master_id, master_ver)
00222 
00223         # no duplicate detected; create a new record for the ID if we don't have one already
00224         if sig:
00225             cur = self.duplicate_db.cursor()
00226             cur.execute('SELECT count(*) FROM crashes WHERE crash_id == ?', [id])
00227             count_id = cur.fetchone()[0]
00228             if count_id == 0:
00229                 cur.execute('INSERT INTO crashes VALUES (?, ?, ?, CURRENT_TIMESTAMP)', (_u(sig), id, None))
00230                 self.duplicate_db.commit()
00231         if addr_sig:
00232             self._duplicate_db_add_address_signature(addr_sig, id)
00233 
00234         return None

Here is the call graph for this function:

def apport.crashdb_impl.launchpad.CrashDatabase.close_duplicate (   self,
  report,
  id,
  master_id 
)
Mark a crash id as duplicate of given master ID.

If master is None, id gets un-duplicated.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 636 of file launchpad.py.

00636 
00637     def close_duplicate(self, report, id, master_id):
00638         '''Mark a crash id as duplicate of given master ID.
00639 
00640         If master is None, id gets un-duplicated.
00641         '''
00642         bug = self.launchpad.bugs[id]
00643 
00644         if master_id:
00645             assert id != master_id, 'cannot mark bug %s as a duplicate of itself' % str(id)
00646 
00647             # check whether the master itself is a dup
00648             master = self.launchpad.bugs[master_id]
00649             if master.duplicate_of:
00650                 master = master.duplicate_of
00651                 master_id = master.id
00652                 if master.id == id:
00653                     # this happens if the bug was manually duped to a newer one
00654                     apport.warning('Bug %i was manually marked as a dupe of newer bug %i, not closing as duplicate',
00655                                    id, master_id)
00656                     return
00657 
00658             for a in bug.attachments:
00659                 if a.title in ('CoreDump.gz', 'Stacktrace.txt',
00660                                'ThreadStacktrace.txt', 'ProcMaps.txt',
00661                                'ProcStatus.txt', 'Registers.txt',
00662                                'Disassembly.txt'):
00663                     try:
00664                         a.removeFromBug()
00665                     except HTTPError:
00666                         pass  # LP#249950 workaround
00667 
00668             bug = self.launchpad.bugs[id]  # fresh bug object, LP#336866 workaround
00669             bug.newMessage(content='Thank you for taking the time to report this crash and helping \
00670 to make this software better.  This particular crash has already been reported and \
00671 is a duplicate of bug #%i, so is being marked as such.  Please look at the \
00672 other bug report to see if there is any missing information that you can \
00673 provide, or to see if there is a workaround for the bug.  Additionally, any \
00674 further discussion regarding the bug should occur in the other report.  \
00675 Please continue to report any other bugs you may find.' % master_id,
00676                            subject='This bug is a duplicate')
00677 
00678             bug = self.launchpad.bugs[id]  # refresh, LP#336866 workaround
00679             if bug.private:
00680                 bug.private = False
00681 
00682             # set duplicate last, since we cannot modify already dup'ed bugs
00683             if not bug.duplicate_of:
00684                 bug.duplicate_of = master
00685 
00686             # cache tags of master bug report instead of performing multiple
00687             # queries
00688             master_tags = master.tags
00689 
00690             if len(master.duplicates) == 10:
00691                 if 'escalation_tag' in self.options and self.options['escalation_tag'] not in master_tags and self.options.get('escalated_tag', ' invalid ') not in master_tags:
00692                     master.tags = master_tags + [self.options['escalation_tag']]  # LP#254901 workaround
00693                     master.lp_save()
00694 
00695                 if 'escalation_subscription' in self.options and self.options.get('escalated_tag', ' invalid ') not in master_tags:
00696                     p = self.launchpad.people[self.options['escalation_subscription']]
00697                     master.subscribe(person=p)
00698 
00699             # requesting updated stack trace?
00700             if report.has_useful_stacktrace() and ('apport-request-retrace' in master_tags
00701                                                    or 'apport-failed-retrace' in master_tags):
00702                 self.update(master_id, report, 'Updated stack trace from duplicate bug %i' % id,
00703                             key_filter=['Stacktrace', 'ThreadStacktrace',
00704                                         'Package', 'Dependencies', 'ProcMaps', 'ProcCmdline'])
00705 
00706                 master = self.launchpad.bugs[master_id]
00707                 x = master.tags[:]  # LP#254901 workaround
00708                 try:
00709                     x.remove('apport-failed-retrace')
00710                 except ValueError:
00711                     pass
00712                 try:
00713                     x.remove('apport-request-retrace')
00714                 except ValueError:
00715                     pass
00716                 master.tags = x
00717                 try:
00718                     master.lp_save()
00719                 except HTTPError:
00720                     pass  # LP#336866 workaround
00721 
00722             # white list of tags to copy from duplicates bugs to the master
00723             tags_to_copy = ['bugpattern-needed', 'running-unity']
00724             for series in self.lp_distro.series:
00725                 if series.status not in ['Active Development',
00726                                          'Current Stable Release', 'Supported']:
00727                     continue
00728                 tags_to_copy.append(series.name)
00729             # copy tags over from the duplicate bug to the master bug
00730             dupe_tags = set(bug.tags)
00731             # reload master tags as they may have changed
00732             master_tags = master.tags
00733             missing_tags = dupe_tags.difference(master_tags)
00734 
00735             for tag in missing_tags:
00736                 if tag in tags_to_copy:
00737                     master_tags.append(tag)
00738 
00739             master.tags = master_tags
00740             master.lp_save()
00741 
00742         else:
00743             if bug.duplicate_of:
00744                 bug.duplicate_of = None
00745 
00746         if bug._dirty_attributes:  # LP#336866 workaround
00747             bug.lp_save()

Here is the call graph for this function:

Download the problem report from given ID and return a Report.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 247 of file launchpad.py.

00247 
00248     def download(self, id):
00249         '''Download the problem report from given ID and return a Report.'''
00250 
00251         report = apport.Report()
00252         b = self.launchpad.bugs[id]
00253 
00254         # parse out fields from summary
00255         m = re.search(r'(ProblemType:.*)$', b.description, re.S)
00256         if not m:
00257             m = re.search(r'^--- \r?$[\r\n]*(.*)', b.description, re.M | re.S)
00258         assert m, 'bug description must contain standard apport format data'
00259 
00260         description = m.group(1).encode('UTF-8').replace('\xc2\xa0', ' ').replace('\r\n', '\n')
00261 
00262         if '\n\n' in description:
00263             # this often happens, remove all empty lines between top and
00264             # 'Uname'
00265             if 'Uname:' in description:
00266                 # this will take care of bugs like LP #315728 where stuff
00267                 # is added after the apport data
00268                 (part1, part2) = description.split('Uname:', 1)
00269                 description = part1.replace('\n\n', '\n') + 'Uname:' \
00270                     + part2.split('\n\n', 1)[0]
00271             else:
00272                 # just parse out the Apport block; e. g. LP #269539
00273                 description = description.split('\n\n', 1)[0]
00274 
00275         report.load(BytesIO(description))
00276 
00277         if 'Date' not in report:
00278             # We had not submitted this field for a while, claiming it
00279             # redundant. But it is indeed required for up-to-the-minute
00280             # comparison with log files, etc. For backwards compatibility with
00281             # those reported bugs, read the creation date
00282             try:
00283                 report['Date'] = b.date_created.ctime()
00284             except AttributeError:
00285                 # support older wadllib API which returned strings
00286                 report['Date'] = b.date_created
00287         if 'ProblemType' not in report:
00288             if 'apport-bug' in b.tags:
00289                 report['ProblemType'] = 'Bug'
00290             elif 'apport-crash' in b.tags:
00291                 report['ProblemType'] = 'Crash'
00292             elif 'apport-kernelcrash' in b.tags:
00293                 report['ProblemType'] = 'KernelCrash'
00294             elif 'apport-package' in b.tags:
00295                 report['ProblemType'] = 'Package'
00296             else:
00297                 raise ValueError('cannot determine ProblemType from tags: ' + str(b.tags))
00298 
00299         report['Tags'] = ' '.join(b.tags)
00300 
00301         if 'Title' in report:
00302             report['OriginalTitle'] = report['Title']
00303 
00304         report['Title'] = b.title
00305 
00306         for attachment in filter_filename(b.attachments):
00307             key, ext = os.path.splitext(attachment.filename)
00308             # ignore attachments with invalid keys
00309             try:
00310                 report[key] = ''
00311             except:
00312                 continue
00313             if ext == '.txt':
00314                 report[key] = attachment.read()
00315             elif ext == '.gz':
00316                 try:
00317                     report[key] = gzip.GzipFile(fileobj=attachment).read()
00318                 except IOError as e:
00319                     # some attachments are only called .gz, but are
00320                     # uncompressed (LP #574360)
00321                     if 'Not a gzip' not in str(e):
00322                         raise
00323                     attachment.seek(0)
00324                     report[key] = attachment.read()
00325             else:
00326                 raise Exception('Unknown attachment type: ' + attachment.filename)
00327         return report

Here is the call graph for this function:

def apport.crashdb.CrashDatabase.duplicate_db_change_master_id (   self,
  old_id,
  new_id 
) [inherited]
Change a crash ID.

Definition at line 332 of file crashdb.py.

00332 
00333     def duplicate_db_change_master_id(self, old_id, new_id):
00334         '''Change a crash ID.'''
00335 
00336         assert self.duplicate_db, 'init_duplicate_db() needs to be called before'
00337 
00338         cur = self.duplicate_db.cursor()
00339         cur.execute('UPDATE crashes SET crash_id = ?, last_change = CURRENT_TIMESTAMP WHERE crash_id = ?',
00340                     [new_id, old_id])
00341         cur.execute('UPDATE address_signatures SET crash_id = ? WHERE crash_id = ?',
00342                     [new_id, old_id])
00343         self.duplicate_db.commit()

def apport.crashdb.CrashDatabase.duplicate_db_fixed (   self,
  id,
  version 
) [inherited]
Mark given crash ID as fixed in the duplicate database.

version specifies the package version the crash was fixed in (None for
'still unfixed').

Definition at line 306 of file crashdb.py.

00306 
00307     def duplicate_db_fixed(self, id, version):
00308         '''Mark given crash ID as fixed in the duplicate database.
00309 
00310         version specifies the package version the crash was fixed in (None for
00311         'still unfixed').
00312         '''
00313         assert self.duplicate_db, 'init_duplicate_db() needs to be called before'
00314 
00315         cur = self.duplicate_db.cursor()
00316         n = cur.execute('UPDATE crashes SET fixed_version = ?, last_change = CURRENT_TIMESTAMP WHERE crash_id = ?',
00317                         (version, id))
00318         assert n.rowcount == 1
00319         self.duplicate_db.commit()

Here is the caller graph for this function:

def apport.crashdb.CrashDatabase.duplicate_db_publish (   self,
  dir 
) [inherited]
Create text files suitable for www publishing.

Create a number of text files in the given directory which Apport
clients can use to determine whether a problem is already reported to
the database, through the known() method. This directory is suitable
for publishing to the web.

The database is indexed by the first two fields of the duplicate or
crash signature, to avoid having to download the entire database every
time.

If the directory already exists, it will be updated. The new content is
built in a new directory which is the given one with ".new" appended,
then moved to the given name in an almost atomic way.

Definition at line 344 of file crashdb.py.

00344 
00345     def duplicate_db_publish(self, dir):
00346         '''Create text files suitable for www publishing.
00347 
00348         Create a number of text files in the given directory which Apport
00349         clients can use to determine whether a problem is already reported to
00350         the database, through the known() method. This directory is suitable
00351         for publishing to the web.
00352 
00353         The database is indexed by the first two fields of the duplicate or
00354         crash signature, to avoid having to download the entire database every
00355         time.
00356 
00357         If the directory already exists, it will be updated. The new content is
00358         built in a new directory which is the given one with ".new" appended,
00359         then moved to the given name in an almost atomic way.
00360         '''
00361         assert self.duplicate_db, 'init_duplicate_db() needs to be called before'
00362 
00363         # first create the temporary new dir; if that fails, nothing has been
00364         # changed and we fail early
00365         out = dir + '.new'
00366         os.mkdir(out)
00367 
00368         # crash addresses
00369         addr_base = os.path.join(out, 'address')
00370         os.mkdir(addr_base)
00371         cur_hash = None
00372         cur_file = None
00373 
00374         cur = self.duplicate_db.cursor()
00375 
00376         cur.execute('SELECT * from address_signatures ORDER BY signature')
00377         for (sig, id) in cur.fetchall():
00378             h = self.duplicate_sig_hash(sig)
00379             if h is None:
00380                 # some entries can't be represented in a single line
00381                 continue
00382             if h != cur_hash:
00383                 cur_hash = h
00384                 if cur_file:
00385                     cur_file.close()
00386                 cur_file = open(os.path.join(addr_base, cur_hash), 'w')
00387 
00388             cur_file.write('%i %s\n' % (id, sig))
00389 
00390         if cur_file:
00391             cur_file.close()
00392 
00393         # duplicate signatures
00394         sig_base = os.path.join(out, 'sig')
00395         os.mkdir(sig_base)
00396         cur_hash = None
00397         cur_file = None
00398 
00399         cur.execute('SELECT signature, crash_id from crashes ORDER BY signature')
00400         for (sig, id) in cur.fetchall():
00401             h = self.duplicate_sig_hash(sig)
00402             if h is None:
00403                 # some entries can't be represented in a single line
00404                 continue
00405             if h != cur_hash:
00406                 cur_hash = h
00407                 if cur_file:
00408                     cur_file.close()
00409                 cur_file = open(os.path.join(sig_base, cur_hash), 'wb')
00410 
00411             cur_file.write(('%i %s\n' % (id, sig)).encode('UTF-8'))
00412 
00413         if cur_file:
00414             cur_file.close()
00415 
00416         # switch over tree; this is as atomic as we can be with directories
00417         if os.path.exists(dir):
00418             os.rename(dir, dir + '.old')
00419         os.rename(out, dir)
00420         if os.path.exists(dir + '.old'):
00421             shutil.rmtree(dir + '.old')

Here is the call graph for this function:

def apport.crashdb.CrashDatabase.duplicate_db_remove (   self,
  id 
) [inherited]
Remove crash from the duplicate database.

This happens when a report got rejected or manually duplicated.

Definition at line 320 of file crashdb.py.

00320 
00321     def duplicate_db_remove(self, id):
00322         '''Remove crash from the duplicate database.
00323 
00324         This happens when a report got rejected or manually duplicated.
00325         '''
00326         assert self.duplicate_db, 'init_duplicate_db() needs to be called before'
00327 
00328         cur = self.duplicate_db.cursor()
00329         cur.execute('DELETE FROM crashes WHERE crash_id = ?', [id])
00330         cur.execute('DELETE FROM address_signatures WHERE crash_id = ?', [id])
00331         self.duplicate_db.commit()

Here is the caller graph for this function:

Return master ID for a duplicate bug.

If the bug is not a duplicate, return None.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 625 of file launchpad.py.

00625 
00626     def duplicate_of(self, id):
00627         '''Return master ID for a duplicate bug.
00628 
00629         If the bug is not a duplicate, return None.
00630         '''
00631         b = self.launchpad.bugs[id].duplicate_of
00632         if b:
00633             return b.id
00634         else:
00635             return None

def apport.crashdb.CrashDatabase.duplicate_sig_hash (   klass,
  sig 
) [inherited]
Create a www/URL proof hash for a duplicate signature

Definition at line 586 of file crashdb.py.

00586 
00587     def duplicate_sig_hash(klass, sig):
00588         '''Create a www/URL proof hash for a duplicate signature'''
00589 
00590         # cannot hash multi-line custom duplicate signatures
00591         if '\n' in sig:
00592             return None
00593 
00594         # custom DuplicateSignatures have a free format, split off first word
00595         i = sig.split(' ', 1)[0]
00596         # standard crash/address signatures use ':' as field separator, usually
00597         # for ExecutableName:Signal
00598         i = '_'.join(i.split(':', 2)[:2])
00599         # we manually quote '/' to make them nicer to read
00600         i = i.replace('/', '_')
00601         i = quote_plus(i.encode('UTF-8'))
00602         # avoid too long file names
00603         i = i[:200]
00604         return i

Here is the caller graph for this function:

Return list of affected source packages for given ID.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 457 of file launchpad.py.

00457 
00458     def get_affected_packages(self, id):
00459         '''Return list of affected source packages for given ID.'''
00460 
00461         bug_target_re = re.compile(
00462             r'/%s/(?:(?P<suite>[^/]+)/)?\+source/(?P<source>[^/]+)$' % self.distro)
00463 
00464         bug = self.launchpad.bugs[id]
00465         result = []
00466 
00467         for task in bug.bug_tasks:
00468             match = bug_target_re.search(task.target.self_link)
00469             if not match:
00470                 continue
00471             if task.status in ('Invalid', "Won't Fix", 'Fix Released'):
00472                 continue
00473             result.append(match.group('source'))
00474         return result

Return the base URL for bug patterns.

See apport.report.Report.search_bug_patterns() for details. If this
function returns None, bug patterns are disabled.

Definition at line 52 of file crashdb.py.

00052 
00053     def get_bugpattern_baseurl(self):
00054         '''Return the base URL for bug patterns.
00055 
00056         See apport.report.Report.search_bug_patterns() for details. If this
00057         function returns None, bug patterns are disabled.
00058         '''
00059         return self.options.get('bug_pattern_url')

def apport.crashdb_impl.launchpad.CrashDatabase.get_comment_url (   self,
  report,
  handle 
)
Return an URL that should be opened after report has been uploaded
and upload() returned handle.

Should return None if no URL should be opened (anonymous filing without
user comments); in that case this function should do whichever
interactive steps it wants to perform.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 205 of file launchpad.py.

00205 
00206     def get_comment_url(self, report, handle):
00207         '''Return an URL that should be opened after report has been uploaded
00208         and upload() returned handle.
00209 
00210         Should return None if no URL should be opened (anonymous filing without
00211         user comments); in that case this function should do whichever
00212         interactive steps it wants to perform.'''
00213 
00214         args = {}
00215         title = report.get('Title', report.standard_title())
00216         if title:
00217             # always use UTF-8 encoding, urlencode() blows up otherwise in
00218             # python 2.7
00219             if type(title) != type(b''):
00220                 title = title.encode('UTF-8')
00221             args['field.title'] = title
00222 
00223         hostname = self.get_hostname()
00224 
00225         project = self.options.get('project')
00226 
00227         if not project:
00228             if 'SourcePackage' in report:
00229                 return 'https://bugs.%s/%s/+source/%s/+filebug/%s?%s' % (
00230                     hostname, self.distro, report['SourcePackage'], handle, urlencode(args))
00231             else:
00232                 return 'https://bugs.%s/%s/+filebug/%s?%s' % (
00233                     hostname, self.distro, handle, urlencode(args))
00234         else:
00235             return 'https://bugs.%s/%s/+filebug/%s?%s' % (
00236                 hostname, project, handle, urlencode(args))

Here is the call graph for this function:

Get 'DistroRelease: <release>' from the given report ID and return
it.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 448 of file launchpad.py.

00448 
00449     def get_distro_release(self, id):
00450         '''Get 'DistroRelease: <release>' from the given report ID and return
00451         it.'''
00452         bug = self.launchpad.bugs[id]
00453         m = re.search('DistroRelease: ([-a-zA-Z0-9.+/ ]+)', bug.description)
00454         if m:
00455             return m.group(1)
00456         raise ValueError('URL does not contain DistroRelease: field')

Return an ID set of all crashes which have not been checked for
being a duplicate.

This is mainly useful for crashes of scripting languages such as
Python, since they do not need to be retraced. It should not return
bugs that are covered by get_unretraced().

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 514 of file launchpad.py.

00514 
00515     def get_dup_unchecked(self):
00516         '''Return an ID set of all crashes which have not been checked for
00517         being a duplicate.
00518 
00519         This is mainly useful for crashes of scripting languages such as
00520         Python, since they do not need to be retraced. It should not return
00521         bugs that are covered by get_unretraced().'''
00522 
00523         try:
00524             bugs = self.lp_distro.searchTasks(tags='need-duplicate-check', created_since='2011-08-01')
00525             return id_set(bugs)
00526         except Exception as e:
00527             apport.error('connecting to Launchpad failed: %s', str(e))
00528             sys.exit(99)  # transient error

Here is the call graph for this function:

Return the package version that fixes a given crash.

Return None if the crash is not yet fixed, or an empty string if the
crash is fixed, but it cannot be determined by which version. Return
'invalid' if the crash report got invalidated, such as closed a
duplicate or rejected.

This function should make sure that the returned result is correct. If
there are any errors with connecting to the crash database, it should
raise an exception (preferably IOError).

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 555 of file launchpad.py.

00555 
00556     def get_fixed_version(self, id):
00557         '''Return the package version that fixes a given crash.
00558 
00559         Return None if the crash is not yet fixed, or an empty string if the
00560         crash is fixed, but it cannot be determined by which version. Return
00561         'invalid' if the crash report got invalidated, such as closed a
00562         duplicate or rejected.
00563 
00564         This function should make sure that the returned result is correct. If
00565         there are any errors with connecting to the crash database, it should
00566         raise an exception (preferably IOError).
00567         '''
00568         # do not do version tracking yet; for that, we need to get the current
00569         # distrorelease and the current package version in that distrorelease
00570         # (or, of course, proper version tracking in Launchpad itself)
00571 
00572         try:
00573             b = self.launchpad.bugs[id]
00574         except KeyError:
00575             return 'invalid'
00576 
00577         if b.duplicate_of:
00578             return 'invalid'
00579 
00580         tasks = list(b.bug_tasks)  # just fetch it once
00581 
00582         if self.distro:
00583             distro_identifier = '(%s)' % self.distro.lower()
00584             fixed_tasks = filter(lambda task: task.status == 'Fix Released' and
00585                                  distro_identifier in task.bug_target_display_name.lower(), tasks)
00586 
00587             if not fixed_tasks:
00588                 fixed_distro = filter(lambda task: task.status == 'Fix Released' and
00589                                       task.bug_target_name.lower() == self.distro.lower(), tasks)
00590                 if fixed_distro:
00591                     # fixed in distro inself (without source package)
00592                     return ''
00593 
00594             if len(fixed_tasks) > 1:
00595                 apport.warning('There is more than one task fixed in %s %s, using first one to determine fixed version', self.distro, id)
00596                 return ''
00597 
00598             if fixed_tasks:
00599                 task = fixed_tasks.pop()
00600                 try:
00601                     return self._get_source_version(task.bug_target_display_name.split()[0])
00602                 except IndexError:
00603                     # source does not exist any more
00604                     return 'invalid'
00605             else:
00606                 # check if there only invalid ones
00607                 invalid_tasks = filter(lambda task: task.status in ('Invalid', "Won't Fix", 'Expired') and
00608                                        distro_identifier in task.bug_target_display_name.lower(), tasks)
00609                 if invalid_tasks:
00610                     non_invalid_tasks = filter(
00611                         lambda task: task.status not in ('Invalid', "Won't Fix", 'Expired') and
00612                         distro_identifier in task.bug_target_display_name.lower(), tasks)
00613                     if not non_invalid_tasks:
00614                         return 'invalid'
00615         else:
00616             fixed_tasks = filter(lambda task: task.status == 'Fix Released', tasks)
00617             if fixed_tasks:
00618                 # TODO: look for current series
00619                 return ''
00620             # check if there any invalid ones
00621             if filter(lambda task: task.status == 'Invalid', tasks):
00622                 return 'invalid'
00623 
00624         return None

Here is the call graph for this function:

Return the hostname for the Launchpad instance.

Definition at line 192 of file launchpad.py.

00192 
00193     def get_hostname(self):
00194         '''Return the hostname for the Launchpad instance.'''
00195 
00196         launchpad_instance = self.options.get('launchpad_instance')
00197         if launchpad_instance:
00198             if launchpad_instance == 'staging':
00199                 hostname = 'staging.launchpad.net'
00200             else:
00201                 hostname = 'launchpad.dev'
00202         else:
00203             hostname = 'launchpad.net'
00204         return hostname

Here is the caller graph for this function:

def apport.crashdb_impl.launchpad.CrashDatabase.get_id_url (   self,
  report,
  id 
)
Return URL for a given report ID.

The report is passed in case building the URL needs additional
information from it, such as the SourcePackage name.

Return None if URL is not available or cannot be determined.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 237 of file launchpad.py.

00237 
00238     def get_id_url(self, report, id):
00239         '''Return URL for a given report ID.
00240 
00241         The report is passed in case building the URL needs additional
00242         information from it, such as the SourcePackage name.
00243 
00244         Return None if URL is not available or cannot be determined.
00245         '''
00246         return 'https://bugs.launchpad.net/bugs/' + str(id)

Return an ID set of all crashes which are not yet fixed.

The list must not contain bugs which were rejected or duplicate.

This function should make sure that the returned list is correct. If
there are any errors with connecting to the crash database, it should
raise an exception (preferably IOError).

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 529 of file launchpad.py.

00529 
00530     def get_unfixed(self):
00531         '''Return an ID set of all crashes which are not yet fixed.
00532 
00533         The list must not contain bugs which were rejected or duplicate.
00534 
00535         This function should make sure that the returned list is correct. If
00536         there are any errors with connecting to the crash database, it should
00537         raise an exception (preferably IOError).'''
00538 
00539         bugs = self.lp_distro.searchTasks(tags='apport-crash')
00540         return id_set(bugs)

Here is the call graph for this function:

Return an ID set of all crashes which have not been retraced yet and
which happened on the current host architecture.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 504 of file launchpad.py.

00504 
00505     def get_unretraced(self):
00506         '''Return an ID set of all crashes which have not been retraced yet and
00507         which happened on the current host architecture.'''
00508         try:
00509             bugs = self.lp_distro.searchTasks(tags=self.arch_tag, created_since='2011-08-01')
00510             return id_set(bugs)
00511         except Exception as e:
00512             apport.error('connecting to Launchpad failed: %s', str(e))
00513             sys.exit(99)  # transient error

Here is the call graph for this function:

def apport.crashdb.CrashDatabase.init_duplicate_db (   self,
  path 
) [inherited]
Initialize duplicate database.

path specifies an SQLite database. It will be created if it does not
exist yet.

Definition at line 76 of file crashdb.py.

00076 
00077     def init_duplicate_db(self, path):
00078         '''Initialize duplicate database.
00079 
00080         path specifies an SQLite database. It will be created if it does not
00081         exist yet.
00082         '''
00083         import sqlite3 as dbapi2
00084 
00085         assert dbapi2.paramstyle == 'qmark', \
00086             'this module assumes qmark dbapi parameter style'
00087 
00088         self.format_version = 3
00089 
00090         init = not os.path.exists(path) or path == ':memory:' or \
00091             os.path.getsize(path) == 0
00092         self.duplicate_db = dbapi2.connect(path, timeout=7200)
00093 
00094         if init:
00095             cur = self.duplicate_db.cursor()
00096             cur.execute('CREATE TABLE version (format INTEGER NOT NULL)')
00097             cur.execute('INSERT INTO version VALUES (?)', [self.format_version])
00098 
00099             cur.execute('''CREATE TABLE crashes (
00100                 signature VARCHAR(255) NOT NULL,
00101                 crash_id INTEGER NOT NULL,
00102                 fixed_version VARCHAR(50),
00103                 last_change TIMESTAMP,
00104                 CONSTRAINT crashes_pk PRIMARY KEY (crash_id))''')
00105 
00106             cur.execute('''CREATE TABLE address_signatures (
00107                 signature VARCHAR(1000) NOT NULL,
00108                 crash_id INTEGER NOT NULL,
00109                 CONSTRAINT address_signatures_pk PRIMARY KEY (signature))''')
00110 
00111             self.duplicate_db.commit()
00112 
00113         # verify integrity
00114         cur = self.duplicate_db.cursor()
00115         cur.execute('PRAGMA integrity_check')
00116         result = cur.fetchall()
00117         if result != [('ok',)]:
00118             raise SystemError('Corrupt duplicate db:' + str(result))
00119 
00120         try:
00121             cur.execute('SELECT format FROM version')
00122             result = cur.fetchone()
00123         except self.duplicate_db.OperationalError as e:
00124             if 'no such table' in str(e):
00125                 # first db format did not have version table yet
00126                 result = [0]
00127         if result[0] > self.format_version:
00128             raise SystemError('duplicate DB has unknown format %i' % result[0])
00129         if result[0] < self.format_version:
00130             print('duplicate db has format %i, upgrading to %i' %
00131                   (result[0], self.format_version))
00132             self._duplicate_db_upgrade(result[0])

Check whether the user is the reporter of given ID.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 475 of file launchpad.py.

00475 
00476     def is_reporter(self, id):
00477         '''Check whether the user is the reporter of given ID.'''
00478 
00479         bug = self.launchpad.bugs[id]
00480         return bug.owner.name == self.launchpad.me.name

Check if the crash db already knows about the crash signature.

Check if the report has a DuplicateSignature, crash_signature(), or
StacktraceAddressSignature, and ask the database whether the problem is
already known. If so, return an URL where the user can check the status
or subscribe (if available), or just return True if the report is known
but there is no public URL. In that case the report will not be
uploaded (i. e. upload() will not be called).

Return None if the report does not have any signature or the crash
database does not support checking for duplicates on the client side.

The default implementation uses a text file format generated by
duplicate_db_publish() at an URL specified by the "dupdb_url" option.
Subclasses are free to override this with a custom implementation, such
as a real database lookup.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 829 of file launchpad.py.

00829 
00830     def known(self, report):
00831         '''Check if the crash db already knows about the crash signature.
00832 
00833         Check if the report has a DuplicateSignature, crash_signature(), or
00834         StacktraceAddressSignature, and ask the database whether the problem is
00835         already known. If so, return an URL where the user can check the status
00836         or subscribe (if available), or just return True if the report is known
00837         but there is no public URL. In that case the report will not be
00838         uploaded (i. e. upload() will not be called).
00839 
00840         Return None if the report does not have any signature or the crash
00841         database does not support checking for duplicates on the client side.
00842 
00843         The default implementation uses a text file format generated by
00844         duplicate_db_publish() at an URL specified by the "dupdb_url" option.
00845         Subclasses are free to override this with a custom implementation, such
00846         as a real database lookup.
00847         '''
00848         # we override the method here to check if the user actually has access
00849         # to the bug, and if the bug requests more retraces; in either case we
00850         # should file it.
00851         url = apport.crashdb.CrashDatabase.known(self, report)
00852 
00853         if not url:
00854             return url
00855 
00856         # record the fact that it is a duplicate, for triagers
00857         report['DuplicateOf'] = url
00858 
00859         try:
00860             f = urlopen(url + '/+text')
00861         except IOError:
00862             # if we are offline, or LP is down, upload will fail anyway, so we
00863             # can just as well avoid the upload
00864             return url
00865 
00866         line = f.readline()
00867         if not line.startswith(b'bug:'):
00868             # presumably a 404 etc. page, which happens for private bugs
00869             return True
00870 
00871         # check tags
00872         for line in f:
00873             if line.startswith(b'tags:'):
00874                 if b'apport-failed-retrace' in line or b'apport-request-retrace' in line:
00875                     return None
00876                 else:
00877                     break
00878 
00879             # stop at the first task, tags are in the first block
00880             if not line.strip():
00881                 break
00882 
00883         return url

Here is the call graph for this function:

Return Launchpad instance.

Definition at line 117 of file launchpad.py.

00117 
00118     def launchpad(self):
00119         '''Return Launchpad instance.'''
00120 
00121         if self.__launchpad:
00122             return self.__launchpad
00123 
00124         if Launchpad is None:
00125             sys.stderr.write('ERROR: The launchpadlib Python module is not installed. This functionality is not available.\n')
00126             sys.exit(1)
00127 
00128         if self.options.get('launchpad_instance'):
00129             launchpad_instance = self.options.get('launchpad_instance')
00130         else:
00131             launchpad_instance = 'production'
00132 
00133         auth_dir = os.path.dirname(self.auth)
00134         if auth_dir and not os.path.isdir(auth_dir):
00135             os.makedirs(auth_dir)
00136 
00137         try:
00138             self.__launchpad = Launchpad.login_with('apport-collect',
00139                                                     launchpad_instance,
00140                                                     launchpadlib_dir=self.__lpcache,
00141                                                     allow_access_levels=['WRITE_PRIVATE'],
00142                                                     credentials_file=self.auth,
00143                                                     version='1.0')
00144         except Exception as e:
00145             if hasattr(e, 'content'):
00146                 msg = e.content
00147             else:
00148                 msg = str(e)
00149             apport.error('connecting to Launchpad failed: %s\nYou can reset the credentials by removing the file "%s"', msg, self.auth)
00150             sys.exit(99)  # transient error
00151 
00152         return self.__launchpad

Here is the call graph for this function:

Definition at line 163 of file launchpad.py.

00163 
00164     def lp_distro(self):
00165         if self.__lp_distro is None:
00166             if self.distro:
00167                 self.__lp_distro = self.launchpad.distributions[self.distro]
00168             elif 'project' in self.options:
00169                 self.__lp_distro = self.launchpad.projects[self.options['project']]
00170             else:
00171                 raise SystemError('distro or project needs to be specified in crashdb options')
00172 
00173         return self.__lp_distro

Here is the caller graph for this function:

Mark a crash id as reintroducing an earlier crash which is
already marked as fixed (having ID 'master').

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 748 of file launchpad.py.

00748 
00749     def mark_regression(self, id, master):
00750         '''Mark a crash id as reintroducing an earlier crash which is
00751         already marked as fixed (having ID 'master').'''
00752 
00753         bug = self.launchpad.bugs[id]
00754         bug.newMessage(content='This crash has the same stack trace characteristics as bug #%i. \
00755 However, the latter was already fixed in an earlier package version than the \
00756 one in this report. This might be a regression or because the problem is \
00757 in a dependent package.' % master,
00758                        subject='Possible regression detected')
00759         bug = self.launchpad.bugs[id]  # fresh bug object, LP#336866 workaround
00760         bug.tags = bug.tags + ['regression-retracer']  # LP#254901 workaround
00761         bug.lp_save()

def apport.crashdb_impl.launchpad.CrashDatabase.mark_retrace_failed (   self,
  id,
  invalid_msg = None 
)
Mark crash id as 'failed to retrace'.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 775 of file launchpad.py.

00775 
00776     def mark_retrace_failed(self, id, invalid_msg=None):
00777         '''Mark crash id as 'failed to retrace'.'''
00778 
00779         bug = self.launchpad.bugs[id]
00780         if invalid_msg:
00781             try:
00782                 task = self._get_distro_tasks(bug.bug_tasks).next()
00783             except StopIteration:
00784                 # no distro task, just use the first one
00785                 task = bug.bug_tasks[0]
00786             task.status = 'Invalid'
00787             task.lp_save()
00788             bug.newMessage(content=invalid_msg,
00789                            subject='Crash report cannot be processed')
00790 
00791             for a in bug.attachments:
00792                 if a.title == 'CoreDump.gz':
00793                     try:
00794                         a.removeFromBug()
00795                     except HTTPError:
00796                         pass  # LP#249950 workaround
00797         else:
00798             if 'apport-failed-retrace' not in bug.tags:
00799                 bug.tags = bug.tags + ['apport-failed-retrace']  # LP#254901 workaround
00800                 bug.lp_save()

Here is the call graph for this function:

Mark crash id as retraced.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 762 of file launchpad.py.

00762 
00763     def mark_retraced(self, id):
00764         '''Mark crash id as retraced.'''
00765 
00766         bug = self.launchpad.bugs[id]
00767         if self.arch_tag in bug.tags:
00768             x = bug.tags[:]  # LP#254901 workaround
00769             x.remove(self.arch_tag)
00770             bug.tags = x
00771             try:
00772                 bug.lp_save()
00773             except HTTPError:
00774                 pass  # LP#336866 workaround

def apport.crashdb.CrashDatabase.set_credentials (   self,
  username,
  password 
) [inherited]
Set username and password.

Definition at line 680 of file crashdb.py.

00680 
00681     def set_credentials(self, username, password):
00682         '''Set username and password.'''
00683 
00684         raise NotImplementedError('this method must be implemented by a concrete subclass')

def apport.crashdb_impl.launchpad.CrashDatabase.update (   self,
  id,
  report,
  comment,
  change_description = False,
  attachment_comment = None,
  key_filter = None 
)
Update the given report ID with all data from report.

This creates a text comment with the "short" data (see
ProblemReport.write_mime()), and creates attachments for all the
bulk/binary data.

If change_description is True, and the crash db implementation supports
it, the short data will be put into the description instead (like in a
new bug).

comment will be added to the "short" data. If attachment_comment is
given, it will be added to the attachment uploads.

If key_filter is a list or set, then only those keys will be added.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 329 of file launchpad.py.

00329 
00330                attachment_comment=None, key_filter=None):
00331         '''Update the given report ID with all data from report.
00332 
00333         This creates a text comment with the "short" data (see
00334         ProblemReport.write_mime()), and creates attachments for all the
00335         bulk/binary data.
00336 
00337         If change_description is True, and the crash db implementation supports
00338         it, the short data will be put into the description instead (like in a
00339         new bug).
00340 
00341         comment will be added to the "short" data. If attachment_comment is
00342         given, it will be added to the attachment uploads.
00343 
00344         If key_filter is a list or set, then only those keys will be added.
00345         '''
00346         bug = self.launchpad.bugs[id]
00347 
00348         if key_filter:
00349             skip_keys = set(report.keys()) - set(key_filter)
00350         else:
00351             skip_keys = None
00352 
00353         # we want to reuse the knowledge of write_mime() with all its different input
00354         # types and output formatting; however, we have to dissect the mime ourselves,
00355         # since we can't just upload it as a blob
00356         mime = tempfile.TemporaryFile()
00357         report.write_mime(mime, skip_keys=skip_keys)
00358         mime.flush()
00359         mime.seek(0)
00360         msg = email.message_from_file(mime)
00361         msg_iter = msg.walk()
00362 
00363         # first part is the multipart container
00364         part = msg_iter.next()
00365         assert part.is_multipart()
00366 
00367         # second part should be an inline text/plain attachments with all short
00368         # fields
00369         part = msg_iter.next()
00370         assert not part.is_multipart()
00371         assert part.get_content_type() == 'text/plain'
00372 
00373         if not key_filter:
00374             # when we update a complete report, we are updating an existing bug
00375             # with apport-collect
00376             x = bug.tags[:]  # LP#254901 workaround
00377             x.append('apport-collected')
00378             # add any tags (like the release) to the bug
00379             if 'Tags' in report:
00380                 x += report['Tags'].lower().split()
00381             bug.tags = x
00382             bug.lp_save()
00383             bug = self.launchpad.bugs[id]  # fresh bug object, LP#336866 workaround
00384 
00385         # short text data
00386         if change_description:
00387             bug.description = bug.description + '\n--- \n' + part.get_payload(decode=True).decode('UTF-8', 'replace')
00388             bug.lp_save()
00389         else:
00390             bug.newMessage(content=part.get_payload(decode=True), subject=comment)
00391 
00392         # other parts are the attachments:
00393         for part in msg_iter:
00394             # print '   attachment: %s...' % part.get_filename()
00395             bug.addAttachment(comment=attachment_comment or '',
00396                               description=part.get_filename(),
00397                               content_type=None,
00398                               data=part.get_payload(decode=True),
00399                               filename=part.get_filename(), is_patch=False)

Here is the caller graph for this function:

def apport.crashdb_impl.launchpad.CrashDatabase.update_traces (   self,
  id,
  report,
  comment = '' 
)
Update the given report ID for retracing results.

This updates Stacktrace, ThreadStacktrace, StacktraceTop,
and StacktraceSource. You can also supply an additional comment.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 400 of file launchpad.py.

00400 
00401     def update_traces(self, id, report, comment=''):
00402         '''Update the given report ID for retracing results.
00403 
00404         This updates Stacktrace, ThreadStacktrace, StacktraceTop,
00405         and StacktraceSource. You can also supply an additional comment.
00406         '''
00407         apport.crashdb.CrashDatabase.update_traces(self, id, report, comment)
00408 
00409         bug = self.launchpad.bugs[id]
00410         # ensure it's assigned to a package
00411         if 'SourcePackage' in report:
00412             for task in bug.bug_tasks:
00413                 if task.target.resource_type_link.endswith('#distribution'):
00414                     task.target = self.lp_distro.getSourcePackage(name=report['SourcePackage'])
00415                     task.lp_save()
00416                     bug = self.launchpad.bugs[id]
00417                     break
00418 
00419         # remove core dump if stack trace is usable
00420         if report.has_useful_stacktrace():
00421             for a in bug.attachments:
00422                 if a.title == 'CoreDump.gz':
00423                     try:
00424                         a.removeFromBug()
00425                     except HTTPError:
00426                         pass  # LP#249950 workaround
00427             try:
00428                 task = self._get_distro_tasks(bug.bug_tasks).next()
00429                 if task.importance == 'Undecided':
00430                     task.importance = 'Medium'
00431                     task.lp_save()
00432             except StopIteration:
00433                 pass  # no distro tasks
00434 
00435             # update bug title with retraced function name
00436             fn = report.stacktrace_top_function()
00437             if fn:
00438                 m = re.match('^(.*crashed with SIG.* in )([^( ]+)(\(\).*$)', bug.title)
00439                 if m and m.group(2) != fn:
00440                     bug.title = m.group(1) + fn + m.group(3)
00441                     try:
00442                         bug.lp_save()
00443                     except HTTPError:
00444                         pass  # LP#336866 workaround
00445                     bug = self.launchpad.bugs[id]
00446 
00447         self._subscribe_triaging_team(bug, report)

Here is the call graph for this function:

def apport.crashdb_impl.launchpad.CrashDatabase.upload (   self,
  report,
  progress_callback = None 
)
Upload given problem report return a handle for it.

This should happen noninteractively.

If the implementation supports it, and a function progress_callback is
passed, that is called repeatedly with two arguments: the number of
bytes already sent, and the total number of bytes to send. This can be
used to provide a proper upload progress indication on frontends.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 174 of file launchpad.py.

00174 
00175     def upload(self, report, progress_callback=None):
00176         '''Upload given problem report return a handle for it.
00177 
00178         This should happen noninteractively.
00179 
00180         If the implementation supports it, and a function progress_callback is
00181         passed, that is called repeatedly with two arguments: the number of
00182         bytes already sent, and the total number of bytes to send. This can be
00183         used to provide a proper upload progress indication on frontends.
00184         '''
00185         assert self.accepts(report)
00186 
00187         blob_file = self._generate_upload_blob(report)
00188         ticket = upload_blob(blob_file, progress_callback, hostname=self.get_hostname())
00189         blob_file.close()
00190         assert ticket
00191         return ticket

Here is the call graph for this function:


Member Data Documentation

Definition at line 112 of file launchpad.py.

Definition at line 113 of file launchpad.py.

Definition at line 114 of file launchpad.py.

Definition at line 107 of file launchpad.py.

Definition at line 109 of file launchpad.py.

Definition at line 48 of file crashdb.py.

Definition at line 101 of file launchpad.py.

Definition at line 50 of file crashdb.py.

Definition at line 87 of file crashdb.py.

Reimplemented from apport.crashdb.CrashDatabase.

Definition at line 108 of file launchpad.py.


The documentation for this class was generated from the following file: