diff --git a/README.rst b/README.rst index 444d76eb..6f9ed356 100644 --- a/README.rst +++ b/README.rst @@ -160,7 +160,40 @@ Queries that have "suppress-graph: true" in them generally should not be removed since we basically want to keep those around, they are persistent infra issues and are not going away. -Steps: +Automated Cleanup +~~~~~~~~~~~~~~~~~ + +#. Run the ``elastic-recheck-cleanup`` command: + + .. code-block:: console + + $ tox -e venv -- elastic-recheck-cleanup -h + ... + usage: elastic-recheck-cleanup [-h] [--bug ] [--dry-run] [-v] + + Remove old queries where the affected projects list the bug status as one + of: Fix Committed, Fix Released + + optional arguments: + -h, --help show this help message and exit + --bug Specific bug number/id to clean. Returns an exit code of + 1 if no query is found for the bug. + --dry-run Print out old queries that would be removed but do not + actually remove them. + -v Print verbose information during execution. + + .. note:: You may want to run with the ``--dry-run`` option first and + sanity check the removed queries before committing them. + +#. Commit the changes and push them up for review: + + .. code-block:: console + + $ git commit -a -m "Remove old queries: `date +%F`" + $ git review -t rm-old-queries + +Manual Cleanup +~~~~~~~~~~~~~~ #. Go to the `All Pipelines `_ page. #. Look for anything that is grayed out at the bottom which means it has not diff --git a/elastic_recheck/cmd/cleanup.py b/elastic_recheck/cmd/cleanup.py new file mode 100644 index 00000000..ff392d11 --- /dev/null +++ b/elastic_recheck/cmd/cleanup.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import argparse +import os + +from elastic_recheck.cmd import graph +from elastic_recheck import elasticRecheck as er + + +# TODO(mriedem): We may want to include Invalid, Incomplete and Won't Fix in +# this list since a bug could be reported against multiple projects but only +# fixed in one of them and marked Invalid in others. +FIXED_STATUSES = ('Fix Committed', 'Fix Released') + + +class InvalidProjectstatus(Exception): + """Indicates an error parsing a bug data's affected projects""" + + +def get_project_status(affected_bug_data): + """Parses a ( - ) value + + :param affected_bug_data: String of the expected form + "( - )". + :raises: InvalidProjectStatus if the affected_bug_data string cannot be + parsed + :returns: Two-item tuple of: + - project + - status + """ + # Note that the string can be "Unknown (Private Bug)" or "Unknown" so + # handle parsing errors. + project_status = affected_bug_data.split(' - ') + if len(project_status) != 2: + raise InvalidProjectstatus(affected_bug_data) + # Trim leading ( and trailing ). + return project_status[0][1:], project_status[1][:-1] + + +def main(): + parser = argparse.ArgumentParser( + description='Remove old queries where the affected projects list the ' + 'bug status as one of: %s' % ', '.join(FIXED_STATUSES)) + parser.add_argument('--bug', metavar='', + help='Specific bug number/id to clean. Returns an ' + 'exit code of 1 if no query is found for the ' + 'bug.') + parser.add_argument('--dry-run', action='store_true', default=False, + help='Print out old queries that would be removed but ' + 'do not actually remove them.') + parser.add_argument('-v', dest='verbose', + action='store_true', default=False, + help='Print verbose information during execution.') + args = parser.parse_args() + verbose = args.verbose + dry_run = args.dry_run + + def info(message): + if verbose: + print(message) + + info('Loading queries') + classifier = er.Classifier('queries') + processed = [] # keep track of the bugs we've looked at + cleaned = [] # keep track of the queries we've removed + for query in classifier.queries: + bug = query['bug'] + processed.append(bug) + + # If we're looking for a specific bug check to see if we found it. + if args.bug and bug != args.bug: + continue + + # Skip anything with suppress-graph: true since those are meant to be + # kept around even if they don't have hits. + if query.get('suppress-graph', False): + info('Skipping query for bug %s since it has ' + '"suppress-graph: true"' % bug) + continue + + info('Getting data for bug: %s' % bug) + bug_data = graph.get_launchpad_bug(bug) + affects = bug_data.get('affects') + # affects is a comma-separated list of ( - ), e.g. + # "(neutron - Confirmed), (nova - Fix Released)". + if affects: + affects = affects.split(',') + fixed_in_all_affected_projects = True + for affected in affects: + affected = affected.strip() + try: + project, status = get_project_status(affected) + if status not in FIXED_STATUSES: + # TODO(mriedem): It could be useful to report queries + # that do not have hits but the bug is not marked as + # fixed. + info('Bug %s is not fixed for project %s' % + (bug, project)) + fixed_in_all_affected_projects = False + break + except InvalidProjectstatus: + print('Unable to parse project status "%s" for bug %s' % + (affected, bug)) + fixed_in_all_affected_projects = False + break + + if fixed_in_all_affected_projects: + # TODO(mriedem): It might be good to sanity check that a query + # does not have hits if we are going to remove it even if the + # bug is marked as fixed, e.g. bug 1745168. The bug may have + # re-appeared, or still be a problem on stable branches, or the + # query may be too broad. + if dry_run: + info('[DRY-RUN] Remove query for bug: %s' % bug) + else: + info('Removing query for bug: %s' % bug) + os.remove('queries/%s.yaml' % bug) + cleaned.append(bug) + else: + print('Unable to determine affected projects for bug %s' % bug) + + # If a specific bug was provided did we find it? + if args.bug and args.bug not in processed: + print('Unable to find query for bug: %s' % args.bug) + return 1 + + # Print a summary of what we cleaned. + prefix = '[DRY-RUN] ' if dry_run else '' + # If we didn't remove anything, just print None. + if not cleaned: + cleaned.append('None') + info('%sRemoved queries:\n%s' % (prefix, '\n'.join(sorted(cleaned)))) + + +if __name__ == "__main__": + main() diff --git a/setup.cfg b/setup.cfg index 3c6fe1e4..a2f0c6c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,7 @@ console_scripts = elastic-recheck-success = elastic_recheck.cmd.check_success:main elastic-recheck-uncategorized = elastic_recheck.cmd.uncategorized_fails:main elastic-recheck-query = elastic_recheck.cmd.query:main + elastic-recheck-cleanup = elastic_recheck.cmd.cleanup:main [upload_sphinx] upload-dir = doc/build/html