diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10eb010 --- /dev/null +++ b/.gitignore @@ -0,0 +1,108 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Vim +*.swp + +# Distribution / packaging +.idea/ +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Custom files +export.csv \ No newline at end of file diff --git a/bz2csv.py b/bz2csv.py new file mode 100644 index 0000000..aa0ddfa --- /dev/null +++ b/bz2csv.py @@ -0,0 +1,174 @@ +import sys +import os +import argparse +import configparser +from datetime import datetime +import bugsy +import csv + + +def map_status(status, mapping_file=None, default="Closed"): + default_status_mapping = { + 'NEW': 'Open', + 'CONFIRMED': 'Open', + 'IN_PROGRESS': 'In_progress', + 'RESOLVED': 'Closed' + } + if mapping_file is not None and os.path.exists(mapping_file): + with open(mapping_file, 'r') as file: + for line in file: + new_status, old_status = line.split('=') + if old_status == status: + return new_status + return map_status(status, default=default, mapping_file=None) + else: + if status not in default_status_mapping: + return default + return default_status_mapping[status] + + +def map_users(username, mapping_file=None, default="admin"): + found_user = None + if mapping_file is None: + return str(username).split('@')[0] + elif mapping_file is not None: + if not os.path.exists(mapping_file): + print("User mapping file not found!") + return None + + with open(mapping_file, 'r') as file: + for line in file: + user, email = line.split('=') + if email == username: + found_user = user + break + if found_user is None: + # if not found in file use default mapping + found_user = map_users(username, mapping_file=None) + else: + return default + return found_user + + +def main(arguments): + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--config-file', required=True) + parser.add_argument('-u', '--users-file', help="You can specify users mapping, by default it will split email and use first part of it\"user123@x.org = user123\"") + parser.add_argument('-s', '--status-file', help="You can specify status mapping. \"new_status=old_status\"") + args = parser.parse_args(arguments) + + settings = configparser.ConfigParser() + settings.read(args.config_file) + has_user_mapping = (args.users_file if args.users_file else None) + has_status_mapping = (args.status_file if args.status_file else None) + + if has_user_mapping: + print("Will be using user mapping file %s" % args.users_file) + if has_status_mapping: + print("Will be using status mapping file %s" % args.status_file) + + # Connect to Bugzilla + bz_url = settings.get('bugzilla', 'url') + bz_user = settings.get('bugzilla', 'user') + bz_pass = settings.get('bugzilla', 'pass') + + # Jira settings + jira_key = settings.get('jira', 'key') + jira_default_type = settings.get('jira', 'def_type') + jira_default_status = settings.get('jira', 'def_status') + jira_default_user = settings.get('jira', 'def_user') + + # CSV settings + csv_filename = settings.get('csv', 'filename') + csv_time_format = settings.get('csv', 'format') + csv_max_comment = settings.get('csv', 'max_comment') + csv_max_attachment = settings.get('csv', 'max_attachment') + csv_advanced_attachment = settings.get('csv', 'advanced_attachment') + + # Check if csv export file does not exists + if os.path.isfile(csv_filename): + os.remove(csv_filename) + + bzapi = bugsy.Bugsy(username=str(bz_user), password=str(bz_pass), bugzilla_url=str(bz_url + "/rest")) + + titles = ["Issue Id", "Category", "Summary", "Description", "Date Created", "Date Modified", "Priority", "Issue type", "Status", "Resolution", "Reporter", "Assignee", "OS", "Label"] + max_attachments = max_comments = 0 + # Obtain Bugzilla bug from gotten products + extra_search = ["creation_time", "last_change_time", "creator", "assigned_to", "priority"] + bugs = bzapi.search_for.include_fields(extra_search).search() + print("Got total of %s issues" % len(bugs)) + + print("Pre-fill titles of Comments and Attachments to csv") + print("Adding %s comments and %s attachments" % (csv_max_comment, csv_max_attachment)) + titles = append_array(titles, "Comments", int(csv_max_comment)) + titles = append_array(titles, "Attachment", int(csv_max_attachment)) + with open(csv_filename, 'a', encoding='utf-8') as csvFile: + writer = csv.writer(csvFile, delimiter=",", quotechar='\"', quoting=csv.QUOTE_ALL) + writer.writerow(titles) + + for bug in bugs: + # bug is object of Bugs more info: https://bugsy.readthedocs.io/en/latest/bug.html#bugsy.Bug.id + _raw = bug.to_dict() + row = [ + "%s-%s" % (str(jira_key).upper(), bug.id), + bug.product, + bug.summary, + bug.summary, + str(datetime.strptime(_raw.get('creation_time')[:-1], '%Y-%m-%dT%H:%M:%S').strftime(csv_time_format)), + str(datetime.strptime(_raw.get('last_change_time')[:-1], '%Y-%m-%dT%H:%M:%S').strftime(csv_time_format)), + _raw.get('priority'), + jira_default_type, + map_status(bug.status, mapping_file=has_status_mapping, default=str(jira_default_status)), + bug.resolution, + map_users(_raw.get('creator'), mapping_file=has_user_mapping, default=jira_default_user), + map_users(_raw.get('assigned_to'), mapping_file=has_user_mapping, default=jira_default_user), + bug.OS, + bug.component + ] + + comments = bug.get_comments() + if len(comments) > max_comments: + max_comments = len(comments) + + # there is specific way jira handle comments + for comment in comments: + time = str(comment.creation_time.strftime(csv_time_format)) + row.append("%s;%s; %s" % (time, map_users(comment.creator, mapping_file=has_user_mapping, default=jira_default_user), comment.text)) + + # Jira require you to fill other comment columns empty + row = append_array(row, "", int(csv_max_comment) - len(comments)) + + # !!! This function wasn't in source code, so I made my own code fix for bugsy + attachments = bug.get_attachments() + if len(attachments) > max_attachments: + max_attachments = len(attachments) + + # there is specific way jira handle attachments + for attach in attachments: + url = "%s/attachment.cgi?id=%s" % (bz_url, attach.get('id')) + if bool(csv_advanced_attachment): + time = str(datetime.strptime(attach.get('creation_time')[:-1], '%Y-%m-%dT%H:%M:%S').strftime(csv_time_format)) + row.append("%s;%s;%s;%s" % (time, map_users(attach.get('creator'), mapping_file=has_user_mapping, default=jira_default_user), attach.get('file_name'), url)) + else: + row.append(url) + + # jira require to pre-fill empty columns + row = append_array(row, "", int(csv_max_attachment) - len(attachments)) + + print("Writing bug #%s to csv" % bug.id) + with open(csv_filename, 'a', encoding='utf-8') as csvFile: + writer = csv.writer(csvFile, delimiter=",", quotechar='\"', quoting=csv.QUOTE_ALL) + writer.writerow(row) + + print("Got max comments: %s" % max_comments) + print("Got max attachments: %s" % max_attachments) + + +def append_array(arr, text, count): + for x in range(count): + arr.append(text) + return arr + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/config b/config new file mode 100644 index 0000000..4ffd049 --- /dev/null +++ b/config @@ -0,0 +1,17 @@ +[bugzilla] +url: https://bz.example.com/bugzilla +user: username@example.com +pass: username_password + +[csv] +filename: export.csv +format: %%d/%%m/%%Y %%H:%%M:%%S +max_comment: 20 +max_attachment: 10 +advanced_attachment: True + +[jira] +key: BZ +def_type: BUG +def_status: Closed +def_user: admin diff --git a/jira_configuration_example.txt b/jira_configuration_example.txt new file mode 100644 index 0000000..70ff9ad --- /dev/null +++ b/jira_configuration_example.txt @@ -0,0 +1,84 @@ +{ + "config.version" : "2.0", + "config.project.from.csv" : "false", + "config.encoding" : "UTF-8", + "config.email.suffix" : "@example.com", + "config.field.mappings" : { + "Status" : { + "jira.field" : "status" + }, + "Assignee" : { + "jira.field" : "assignee" + }, + "Category" : { + "jira.field" : "components" + }, + "Description" : { + "jira.field" : "description" + }, + "Issue type" : { + "jira.field" : "issuetype" + }, + "OS" : { + "jira.field" : "versions" + }, + "Priority" : { + "jira.field" : "priority" + }, + "Comments" : { + "jira.field" : "comment" + }, + "Reporter" : { + "jira.field" : "reporter" + }, + "Label" : { + "jira.field" : "labels" + }, + "Attachment" : { + "jira.field" : "attachment" + }, + "Date Modified" : { + "jira.field" : "updated" + }, + "Summary" : { + "jira.field" : "summary" + }, + "Issue Id" : { + "jira.field" : "issue-id" + }, + "Date Created" : { + "jira.field" : "created" + }, + "Resolution" : { + "jira.field" : "resolution" + } + }, + "config.value.mappings" : { + "Status" : { + "Closed" : "10001", + "In_progress" : "3", + "Open" : "10000" + }, + "Issue type" : { + "BUG" : "10005" + }, + "Resolution" : { + "DUPLICATE" : "10002", + "WORKSFORME" : "10102", + "WONTFIX" : "10001", + "FIXED" : "10000", + "INVALID" : "10100", + "SOFTWARE" : "10104" + } + }, + "config.delimiter" : ",", + "config.project" : { + "project.type" : null, + "project.key" : "HW", + "project.description" : null, + "project.url" : null, + "project.name" : "Project Name", + "project.lead" : null + }, + "config.date.format" : "dd/MM/yy HH:mm:ss" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e08aa1c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +certifi==2018.1.18 +chardet==3.0.4 +defusedxml==0.5.0 +idna==2.6 +oauthlib==2.0.6 +ordereddict==1.1 +pbr==3.1.1 +bugsy>=0.10.1 +requests==2.20.0 +requests-oauthlib==0.8.0 +requests-toolbelt==0.8.0 +six==1.11.0 +urllib3==1.24.2 diff --git a/status-mapping.cfg b/status-mapping.cfg new file mode 100644 index 0000000..f275b11 --- /dev/null +++ b/status-mapping.cfg @@ -0,0 +1 @@ +test_guy=bugzilla_user@example.com \ No newline at end of file diff --git a/user-mapping.cfg b/user-mapping.cfg new file mode 100644 index 0000000..8ba7443 --- /dev/null +++ b/user-mapping.cfg @@ -0,0 +1 @@ +druvisvi=hw@mikrotik.com \ No newline at end of file