]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/script/backport-create-issue
import 15.2.0 Octopus source
[ceph.git] / ceph / src / script / backport-create-issue
index c9954af3cfebe9f5f510ad3fc8f5a33bafa802ba..a599bd305f71876e89626db1a1bb36272c1bf1c2 100755 (executable)
@@ -14,7 +14,7 @@
 #
 # 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
 #
 import argparse
 import logging
+import os
 import re
 import time
 from redminelib import Redmine  # https://pypi.org/project/python-redmine/
 
-redmine_endpoint = "http://tracker.ceph.com"
+redmine_endpoint = "https://tracker.ceph.com"
 project_name = "Ceph"
 release_id = 16
 delay_seconds = 5
+redmine_key_file="~/.redmine_key"
 #
 # NOTE: release_id is hard-coded because
 # http://www.redmine.org/projects/redmine/wiki/Rest_CustomFields
@@ -53,25 +55,30 @@ status2status_id = {}
 project_id2project = {}
 tracker2tracker_id = {}
 version2version_id = {}
+resolve_parent = None
 
 def usage():
-    logging.error("Command-line arguments must include either a Redmine key (--key) "
+    logging.error("Redmine credentials are required to perform this operation. "
+                  "Please provide either a Redmine key (via %s) "
                   "or a Redmine username and password (via --user and --password). "
                   "Optionally, one or more issue numbers can be given via positional "
                   "argument(s). In the absence of positional arguments, the script "
-                  "will loop through all issues in Pending Backport status.")
+                  "will loop through all issues in Pending Backport status." % redmine_key_file)
     exit(-1)
 
 def parse_arguments():
     parser = argparse.ArgumentParser()
     parser.add_argument("issue_numbers", nargs='*', help="Issue number")
-    parser.add_argument("--key", help="Redmine user key")
     parser.add_argument("--user", help="Redmine user")
     parser.add_argument("--password", help="Redmine password")
+    parser.add_argument("--resolve-parent", help="Resolve parent issue if all backports resolved/rejected",
+                        action="store_true")
     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")
+    parser.add_argument("--force", help="Create backport issues even if status not Pending Backport",
+                        action="store_true")
     return parser.parse_args()
 
 def set_logging_level(a):
@@ -83,25 +90,39 @@ def set_logging_level(a):
 
 def report_dry_run(a):
     if a.dry_run:
-         logging.info("Dry run: nothing will be written to Redmine")
+        logging.info("Dry run: nothing will be written to Redmine")
     else:
-         logging.warning("Missing issues will be created in Backport tracker "
+        logging.warning("Missing issues will be created in Backport tracker "
                          "of the relevant Redmine project")
 
+def process_resolve_parent_option(a):
+    global resolve_parent
+    resolve_parent = a.resolve_parent
+    if a.resolve_parent:
+        logging.warning("Parent issues with all backports resolved/rejected will be marked Resolved")
+
 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:
+    full_path=os.path.expanduser(redmine_key_file)
+    redmine_key=''
+    try:
+        with open(full_path, "r") as f:
+            redmine_key = f.read().strip()
+    except FileNotFoundError:
+        pass
+
+    if a.user and a.password:
         logging.info("Redmine username and password were provided; using them")
         return Redmine(redmine_endpoint, username=a.user, password=a.password)
+    elif redmine_key:
+        logging.info("Redmine key was read from '%s'; using it" % redmine_key_file)
+        return Redmine(redmine_endpoint, key=redmine_key)
     else:
         usage()
 
 def releases():
     return ('argonaut', 'bobtail', 'cuttlefish', 'dumpling', 'emperor',
             'firefly', 'giant', 'hammer', 'infernalis', 'jewel', 'kraken',
-            'luminous', 'mimic')
+            'luminous', 'mimic', 'nautilus')
 
 def populate_status_dict(r):
     for status in r.issue_status.all():
@@ -154,8 +175,10 @@ def get_release(issue):
             return field['value']
 
 def update_relations(r, issue, dry_run):
+    global resolve_parent
     relations = r.issue_relation.filter(issue_id=issue['id'])
     existing_backports = set()
+    existing_backports_dict = {}
     for relation in relations:
         other = r.issue.get(relation['issue_to_id'])
         if other['tracker']['name'] != 'Backport':
@@ -173,10 +196,13 @@ def update_relations(r, issue, dry_run):
                           " backport issue detected")
             continue
         existing_backports.add(release)
+        existing_backports_dict[release] = relation['issue_to_id']
         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")
+        if resolve_parent:
+            maybe_resolve(issue, existing_backports_dict, dry_run)
         return None
     if existing_backports.issuperset(issue['backports']):
         logging.error(url(issue) + " has more backport issues (" +
@@ -189,7 +215,7 @@ def update_relations(r, issue, dry_run):
             logging.error(url(issue) + " requires backport to " +
                 "unknown release " + release)
             break
-        subject = release + ": " + issue['subject']
+        subject = (release + ": " + issue['subject'])[:255]
         if dry_run:
             logging.info(url(issue) + " add backport to " + release)
             continue
@@ -211,13 +237,54 @@ def update_relations(r, issue, dry_run):
                      release + " " + url(other))
     return None
 
-def iterate_over_backports(r, issues, dry_run):
+def maybe_resolve(issue, backports, dry_run):
+    '''
+    issue is a parent issue in Pending Backports status, and backports is a dict
+    like, e.g., { "luminous": 25345, "mimic": 32134 }.
+    If all the backport issues are Resolved/Rejected, set the parent issue to Resolved, too.
+    '''
+    global delay_seconds
+    global redmine
+    global status2status_id
+    if not backports:
+        return None
+    pending_backport_status_id = status2status_id["Pending Backport"]
+    resolved_status_id = status2status_id["Resolved"]
+    rejected_status_id = status2status_id["Rejected"]
+    logging.debug("entering maybe_resolve with parent issue ->{}<- backports ->{}<-"
+                  .format(issue.id, backports))
+    assert issue.status.id == pending_backport_status_id, \
+        "Parent Redmine issue ->{}<- has status ->{}<- (expected Pending Backport)".format(issue.id, issue.status)
+    all_resolved = True
+    resolved_equiv_statuses = [resolved_status_id, rejected_status_id]
+    for backport in backports.keys():
+        tracker_issue_id = backports[backport]
+        backport_issue = redmine.issue.get(tracker_issue_id)
+        logging.debug("{} backport is in status {}".format(backport, backport_issue.status.name))
+        if backport_issue.status.id not in resolved_equiv_statuses:
+            all_resolved = False
+            break
+    if all_resolved:
+        logging.debug("Parent ->{}<- all backport issues in status Resolved".format(url(issue)))
+        note = ("While running with --resolve-parent, the script \"backport-create-issue\" "
+                "noticed that all backports of this issue are in status \"Resolved\" or \"Rejected\".")
+        if dry_run:
+            logging.info("Set status of parent ->{}<- to Resolved".format(url(issue)))
+        else:
+            redmine.issue.update(issue.id, status_id=resolved_status_id, notes=note)
+            logging.info("Parent ->{}<- status changed from Pending Backport to Resolved".format(url(issue)))
+            logging.debug("Rate-limiting to avoid seeming like a spammer")
+            time.sleep(delay_seconds)
+    else:
+        logging.debug("Some backport issues are still unresolved: leaving parent issue open")
+
+def iterate_over_backports(r, issues, dry_run=False):
     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)
+        print('Examining issue#{} ({}/{})\r'.format(issue.id, counter, len(issues)), 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),
@@ -229,14 +296,15 @@ def iterate_over_backports(r, issues, dry_run):
         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))
+    print('                                     \r', end='', flush=True)
+    logging.info("Processed {} issues".format(counter))
     return None
 
 
 if __name__ == '__main__':
     args = parse_arguments()
     set_logging_level(args)
+    process_resolve_parent_option(args)
     report_dry_run(args)
     redmine = connect_to_redmine(args)
     project = redmine.project.get(project_name)
@@ -247,13 +315,29 @@ if __name__ == '__main__':
     logging.debug("Pending Backport status has ID {}"
         .format(pending_backport_status_id))
     populate_tracker_dict(redmine)
+    force_create = False
     if args.issue_numbers:
         issue_list = ','.join(args.issue_numbers)
         logging.info("Processing issue list ->{}<-".format(issue_list))
-        issues = redmine.issue.filter(project_id=ceph_project_id,
-                                      issue_id=issue_list,
-                                      status_id=pending_backport_status_id)
+        if args.force:
+            force_create = True
+            logging.warn("--force option was given: ignoring issue status!")
+            issues = redmine.issue.filter(project_id=ceph_project_id,
+                                          issue_id=issue_list)
+
+        else:
+            issues = redmine.issue.filter(project_id=ceph_project_id,
+                                          issue_id=issue_list,
+                                          status_id=pending_backport_status_id)
     else:
+        if args.force:
+            logging.warn("ignoring --force option, which can only be used with an explicit issue list")
         issues = redmine.issue.filter(project_id=ceph_project_id,
                                       status_id=pending_backport_status_id)
-    iterate_over_backports(redmine, issues, args.dry_run)
+    if force_create:
+        logging.info("Processing {} issues regardless of status"
+                     .format(len(issues)))
+    else:
+        logging.info("Processing {} issues with status Pending Backport"
+                     .format(len(issues)))
+    iterate_over_backports(redmine, issues, dry_run=args.dry_run)