]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
scripts: backport-create-issue (initial commit)
authorNathan Cutler <ncutler@suse.com>
Tue, 17 Apr 2018 19:12:45 +0000 (21:12 +0200)
committerNathan Cutler <ncutler@suse.com>
Tue, 17 Apr 2018 21:19:41 +0000 (23:19 +0200)
This is a standalone implementation of a script written by Loic Dachary.

Signed-off-by: Nathan Cutler <ncutler@suse.com>
src/script/backport-create-issue [new file with mode: 0755]

diff --git a/src/script/backport-create-issue b/src/script/backport-create-issue
new file mode 100755 (executable)
index 0000000..f95c2ce
--- /dev/null
@@ -0,0 +1,230 @@
+#!/usr/bin/env python3
+#
+# backport-create-issue
+#
+# Standalone version of the "backport-create-issue" subcommand of
+# "ceph-workbench" by Loic Dachary.
+#
+# This script scans Redmine (tracker.ceph.com) for issues in "Pending Backport"
+# status and creates backport issues for them, based on the contents of the
+# "Backport" field while trying to avoid creating duplicate backport issues.
+#
+# Copyright (C) 2015 <contact@redhat.com>
+# Copyright (C) 2018, SUSE LLC
+#
+# Author: Loic Dachary <loic@dachary.org>
+# Author: Nathan Cutler <ncutler@suse.com>
+# 
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see http://www.gnu.org/licenses/>
+#
+import argparse
+import logging
+import re
+from redminelib import Redmine  # https://pypi.org/project/python-redmine/
+
+redmine_endpoint = "http://tracker.ceph.com"
+project_name = "Ceph"
+release_id = 16
+status2status_id = {}
+project_id2project = {}
+tracker2tracker_id = {}
+version2version_id = {}
+
+def usage():
+    logging.error("Command-line arguments must include either a Redmine key (positional "
+                  "argument) or a Redmine username and password (via --user and --password)")
+    exit(-1)
+
+def parse_arguments():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("key", nargs='?', help="Redmine user key")
+    parser.add_argument("--user", help="Redmine user")
+    parser.add_argument("--password", help="Redmine password")
+    parser.add_argument("--debug", help="Show debug-level messages",
+                        action="store_true")
+    parser.add_argument("--dry-run", help="Do not write anything to Redmine",
+                        action="store_true")
+    return parser.parse_args()
+
+def set_logging_level(a):
+    if a.debug:
+        logging.basicConfig(level=logging.DEBUG)
+    else:
+        logging.basicConfig(level=logging.INFO)
+    return None
+
+def report_dry_run(a):
+    if a.dry_run:
+         logging.info("Dry run: nothing will be written to Redmine")
+    else:
+         logging.warning("Missing issues will be created in Backport tracker "
+                         "of the relevant Redmine project")
+
+def connect_to_redmine(a):
+    if a.key:
+        logging.info("Redmine key was provided; using it")
+        return Redmine(redmine_endpoint, key=a.key)
+    elif a.user and a.password:
+        logging.info("Redmine username and password were provided; using them")
+        return Redmine(redmine_endpoint, user=a.key, password=a.password)
+    else:
+        usage()
+
+def releases():
+    return ('argonaut', 'bobtail', 'cuttlefish', 'dumpling', 'emperor',
+            'firefly', 'giant', 'hammer', 'infernalis', 'jewel', 'kraken',
+            'luminous')
+
+def populate_status_dict(r):
+    for status in r.issue_status.all():
+        status2status_id[status.name] = status.id
+    logging.debug("Statuses {}".format(status2status_id))
+    return None
+
+def populate_version_dict(r, p_id):
+    versions = r.version.filter(project_id=p_id)
+    for version in versions:
+        version2version_id[version.name] = version.id
+    #logging.debug("Versions {}".format(version2version_id))
+    return None
+
+def populate_tracker_dict(r):
+    for tracker in r.tracker.all():
+        tracker2tracker_id[tracker.name] = tracker.id
+    logging.debug("Trackers {}".format(tracker2tracker_id))
+    return None
+
+def has_tracker(r, p_id, tracker_name):
+    for tracker in get_project(r, p_id).trackers:
+        if tracker['name'] == tracker_name:
+            return True
+    return False
+
+def get_project(r, p_id):
+    if p_id not in project_id2project:
+        p_obj = r.project.get(p_id, include='trackers')
+        project_id2project[p_id] = p_obj
+    return project_id2project[p_id]
+
+def url(issue):
+    return redmine_endpoint + "/issues/" + str(issue['id'])
+
+def set_backport(issue):
+    for field in issue['custom_fields']:
+        if field['name'] == 'Backport' and field['value'] != 0:
+            issue['backports'] = set(re.findall('\w+', field['value']))
+            logging.debug("backports for " + str(issue['id']) +
+                          " is " + str(field['value']) + " " +
+                          str(issue['backports']))
+            return True
+    return False
+
+def get_release(issue):
+    for field in issue.custom_fields:
+        if field['name'] == 'Release':
+            return field['value']
+
+def update_relations(r, issue, dry_run):
+    success = True
+    relations = r.issue_relation.filter(issue_id=issue['id'])
+    existing_backports = set()
+    for relation in relations:
+        other = r.issue.get(relation['issue_to_id'])
+        if other['tracker']['name'] != 'Backport':
+            logging.debug(url(issue) + " ignore relation to " +
+                url(other) + " because it is not in the Backport " +
+                "tracker")
+            continue
+        if relation['relation_type'] != 'copied_to':
+            logging.error(self.url(issue) + " unexpected relation '" +
+                relation['relation_type'] + "' to " + self.url(other))
+            success = False
+            continue
+        release = get_release(other)
+        existing_backports.add(release)
+        logging.debug(url(issue) + " backport to " + release + " is " +
+            redmine_endpoint + "/issues/" + str(relation['issue_to_id']))
+    if existing_backports == issue['backports']:
+        logging.debug(url(issue) + " has all the required backport issues")
+        return success
+    if existing_backports.issuperset(issue['backports']):
+        logging.error(url(issue) + " has more backport issues (" +
+            ",".join(sorted(existing_backports)) + ") than expected (" +
+            ",".join(sorted(issue['backports'])) + ")")
+        return False
+    backport_tracker_id = tracker2tracker_id['Backport']
+    for release in issue['backports'] - existing_backports:
+        if release not in releases():
+            logging.error(url(issue) + " requires backport to " +
+                "unknown release " + release)
+            success = False
+            break
+        subject = release + ": " + issue['subject']
+        if dry_run:
+            logging.info(url(issue) + " add backport to " + release)
+            continue
+        other = r.issue.create(project_id=issue['project']['id'],
+                               tracker_id=backport_tracker_id,
+                               subject=subject,
+                               priority='Normal',
+                               target_version=None,
+                               custom_fields=[{
+                                   "id": release_id,
+                                   "value": release,
+                               }])
+        r.issue_relation.create(issue_id=issue['id'],
+                                issue_to_id=other['id'],
+                                relation_type='copied_to')
+        logging.info(url(issue) + " added backport to " +
+                     release + " " + url(other))
+    return success
+
+def iterate_over_backports(r, issues, dry_run):
+    counter = 0
+    for issue in issues:
+        counter += 1
+        logging.debug("{} ({}) {}".format(issue.id, issue.project,
+            issue.subject))
+        print('{}\r'.format(issue.id), end='', flush=True)
+        if not has_tracker(r, issue['project']['id'], 'Backport'):
+            logging.info("{} skipped because the project {} does not "
+                "have a Backport tracker".format(url(issue),
+                issue['project']['name']))
+            continue
+        if not set_backport(issue):
+            logging.error(url(issue) + " no backport field")
+            continue
+        if len(issue['backports']) == 0:
+            logging.error(url(issue) + " the backport field is empty")
+        update_relations(r, issue, dry_run)
+    logging.info("Processed {} issues with status Pending Backport"
+                 .format(counter))
+    return None
+
+
+args = parse_arguments()
+set_logging_level(args)
+report_dry_run(args)
+redmine = connect_to_redmine(args)
+project = redmine.project.get(project_name)
+ceph_project_id = project.id
+logging.debug("Project {} has ID {}".format(project_name, ceph_project_id))
+populate_status_dict(redmine)
+pending_backport_status_id = status2status_id["Pending Backport"]
+logging.debug("Pending Backport status has ID {}".format(pending_backport_status_id))
+#populate_version_dict(redmine, ceph_project_id)
+populate_tracker_dict(redmine)
+issues = redmine.issue.filter(project_id=ceph_project_id,
+                              status_id=pending_backport_status_id)
+iterate_over_backports(redmine, issues, args.dry_run)