/
githubmirror
/
libreport
Обзор
Документация
Войти
/
githubmirror
/
libreport
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/plugins/python/reporter_bugzilla.py
927 строк
36 KB
Michal Srb
reporter-bugzilla: Make the supplementary bugs easily identifiable
18 фев 2024, 16:32
18 фев 2024, 16:32
f7a0bba
Код
Авторство
О чём код?
#!/usr/bin/python3 # Copyright (C) 2022 ABRT team # Copyright (C) 2022 Red Hat Inc # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import configparser import getopt import gettext import locale import logging import os import re import stat import sys from getpass import getpass from logging import getLogger from typing import Any, Dict, List import reportclient.internal.const as const from reportclient.internal.bz_connection import BZConnection from reportclient.internal.configuration_files import ConfFileLoader from reportclient.internal.dump_dir import DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE, DD_FAIL_QUIETLY_ENOENT, DumpDir from reportclient.internal.global_configuration import GlobalConfFileLoader from reportclient.internal.problem_data import ProblemDataLoader from reportclient.internal.problem_formatter import ProblemFormatter from reportclient.internal.problem_utils import os_info_get_value, pd_get_item, pd_get_item_content from reportclient.internal.report_result import report_result_new_with_label_from_env from reportclient.internal.utils import string_to_bool DEFAULT_BUGZILLA_PRODUCT = 'Fedora' G_MAXUINT64 = 18446744073709551615 _ = gettext.gettext logger = getLogger(__name__) def logger_init(logger, level): handler = logging.StreamHandler(stream=sys.stderr) handler.setLevel(level) logger.addHandler(handler) logger.setLevel(level) def init_gettext(): try: locale.setlocale(locale.LC_ALL, '') except locale.Error: os.environ['LC_ALL'] = 'C' locale.setlocale(locale.LC_ALL, '') gettext.bindtextdomain(const.GETTEXT_PROGNAME, '/usr/share/locale') gettext.textdomain(const.GETTEXT_PROGNAME) def set_default_settings(os_info: List[str], settings: Dict): # if BugzillaURL is defined in conf_file or env, it will replace this value settings['BugzillaURL'] = os_info_get_value('BUG_REPORT_URL', os_info) logger.debug("Loaded BUG_REPORT_URL '%s' from os-release", settings['BugzillaURL']) # if Product or ProductVersion is defined in conf_file or env, it will replace this value settings['Product'] = os_info_get_value('REDHAT_BUGZILLA_PRODUCT', os_info) settings['ProductVersion'] = os_info_get_value('REDHAT_BUGZILLA_PRODUCT_VERSION', os_info) logger.debug("Loaded Product '%s' from os-release", settings['Product']) logger.debug("Loaded ProductVersion '%s' from os-release", settings['ProductVersion']) def set_settings(bz: Dict, settings: Dict): environ = os.environ.get('Bugzilla_APIKey') if not environ: environ = settings.get('APIKey') bz['b_api_key'] = environ or '' environ = os.environ.get("Bugzilla_BugzillaURL") if not environ: environ = settings.get('BugzillaURL') bz['b_bugzilla_url'] = environ or 'https://bugzilla.redhat.com' bz['b_bugzilla_url'] = bz['b_bugzilla_url'].rstrip('/') bz['b_bugzilla_xmlrpc'] = os.path.join(bz['b_bugzilla_url'], "xmlrpc.cgi") bz['b_bugzilla_rest'] = os.path.join(bz['b_bugzilla_url'], "rest.cgi") environ = os.environ.get("Bugzilla_Product") if environ: bz['b_product'] = environ environ = os.environ.get("Bugzilla_ProductVersion") if environ: bz['b_product_version'] = environ else: option = settings.get('Product') if option: bz['b_product'] = option option = settings.get('ProductVersion') if option: bz['b_product_version'] = option environ = os.environ.get("Bugzilla_SSLVerify") if not environ: environ = settings.get("SSLVerify") bz['b_ssl_verify'] = string_to_bool(environ) environ = os.environ.get('Bugzilla_DontMatchComponents') if not environ: environ = settings.get('DontMatchComponents') bz['b_DontMatchComponents'] = environ or '' if not bz.get('b_create_private'): bz['b_create_private'] = False # env var has the highest priority b_create_private = os.environ.get('ABRT_CREATE_PRIVATE_TICKET') if not b_create_private: # 2nd highest priority b_create_private = os.environ.get("Bugzilla_CreatePrivate") if not b_create_private: # value from the config file b_create_private = settings.get('Bugzilla_CreatePrivate') if b_create_private: bz['b_create_private'] = string_to_bool(b_create_private) if bz.get('b_create_private'): logger.info('create private bz ticket: YES') else: logger.info('create private bz ticket: NO') environ = os.environ.get('Bugzilla_PrivateGroups') if not environ: environ = settings.get('Bugzilla_PrivateGroups') if environ: groups = [i.strip() for i in environ.split(',')] else: groups = [] if not bz.get('b_private_groups'): bz['b_private_groups'] = groups logger.info("groups: '%s'", bz['b_private_groups']) elif groups: logger.warning(_("Warning, private ticket groups already specified as cmdline argument, ignoring the env variable and configuration")) def abrt_init(): if not global_config.libreport_load_global_configuration(): logger.error("Cannot continue without libreport global configuration.") sys.exit(1) env_verbose = os.environ.get('ABRT_VERBOSE') if env_verbose: verbose = int(env_verbose) else: verbose = 0 progname = os.path.basename(sys.argv[0]) pfx = os.environ.get('ABRT_PROG_PREFIX') if pfx: msg_prefix = g_progname return (progname, verbose) def export_abrt_envvars(pfx=0): os.environ['ABRT_VERBOSE'] = str(g_verbose) if pfx: os.environ['ABRT_PROG_PREFIX'] = 1 msg_prefix = g_progname def find_in_reported_to(problem_dir: str, reportee: str): try: with open(f"{problem_dir}/reported_to", 'r', encoding='utf-8') as handle: lines = handle.read().split('\n') except (FileNotFoundError, PermissionError): pass for line in lines: key, value = line.split(': ', 1) if key == reportee: return value return None def ask_password(message): """Ask for a password, with echo turned off. :param message: str, prompt message :return: str, password """ is_slave_mode = bool(os.environ.get('REPORT_CLIENT_SLAVE') is not None) is_noninteractive_mode = bool(os.environ.get('REPORT_CLIENT_NONINTERACTIVE') is not None) if is_slave_mode: # See src/include/client.h for details about the libreport communication protocol print(f'ASK_PASSWORD {message}') sys.stdout.flush() return getpass() if is_noninteractive_mode: print() return '' return getpass(prompt=f'{message}') def ask_yes_no(question): yes = _("y") no = _("N") is_slave_mode = bool(os.environ.get('REPORT_CLIENT_SLAVE') is not None) is_noninteractive_mode = bool(os.environ.get("REPORT_CLIENT_NONINTERACTIVE") is not None) env_response = os.environ.get('REPORT_CLIENT_RESPONSE') if env_response and env_response.lower() == yes: return True if is_slave_mode: print(f'ASK_YES_NO {question}') else: print(f'{question} [{yes}/{no}] ', end='') sys.stdout.flush() if not is_slave_mode and is_noninteractive_mode: print() sys.stdout.flush() return False try: answer = input() except (EOFError, KeyboardInterrupt): sys.exit(1) if not answer: return False return (is_slave_mode and answer[0] == 'y') or answer.lower() == yes def ask(question): is_slave_mode = bool(os.environ.get('REPORT_CLIENT_SLAVE') is not None) is_noninteractive_mode = bool(os.environ.get("REPORT_CLIENT_NONINTERACTIVE") is not None) if is_slave_mode: print(f'ASK {question}') else: print(f'{question} ', end='') sys.stdout.flush() if not is_slave_mode and is_noninteractive_mode: print() sys.stdout.flush() return '' try: answer = input() except (EOFError, KeyboardInterrupt): sys.exit(1) return answer def log_out(bug_info, rhbz, dump_dir_name): if bug_info.get('bi_resolution'): resolution = f" {bug_info['bi_resolution']}" else: resolution = '' logger.warning(_("Status: %s%s %s/show_bug.cgi?id=%u"), bug_info['bi_status'], resolution, rhbz['b_bugzilla_url'], bug_info['bi_id']) dump_dir_obj = DumpDir(logger) dd = dump_dir_obj.dd_opendir(dump_dir_name, 0) if dd: result = report_result_new_with_label_from_env("Bugzilla") result['url'] = f"{rhbz['b_bugzilla_url']}/show_bug.cgi?id={bug_info['bi_id']}" dump_dir_obj.libreport_add_reported_to_entry(dd, result) dump_dir_obj.dd_close(dd) sys.exit(0) if __name__ == '__main__': global_config = GlobalConfFileLoader(logger) g_progname, g_verbose = abrt_init() # localization init_gettext() config = configparser.ConfigParser() user_conf = os.path.join(os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config')), 'libreport', 'bugzilla.conf') program_usage_string = _( "Usage:" "\n{0} [-vbf] [-g GROUP-NAME]... [-c CONFFILE]... [-F FMTFILE] [-A FMTFILE2] -d DIR" "\nor:" "\n{0} [-v] [-c CONFFILE]... [-d DIR] -t[ID] FILE..." "\nor:" "\n{0} [-v] [-c CONFFILE]... [-d DIR] -t[ID] -w" "\nor:" "\n{0} [-v] [-c CONFFILE]... -h DUPHASH [-p[PRODUCT]]" "\n" "\nReports problem to Bugzilla." "\n" "\nThe tool reads DIR. Then it logs in to Bugzilla and tries to find a bug" "\nwith the same abrt_hash:HEXSTRING in 'Whiteboard'." "\n" "\nIf such bug is not found, then a new bug is created. Elements of DIR" "\nare stored in the bug as part of bug description or as attachments," "\ndepending on their type and size." "\n" "\nOtherwise, if such bug is found and it is marked as CLOSED DUPLICATE," "\nthe tool follows the chain of duplicates until it finds a non-DUPLICATE bug." "\nThe tool adds a new comment to found bug." "\n" "\nThe URL to new or modified bug is printed to stdout and recorded in" "\n'reported_to' element." "\n" "\nOption -t uploads FILEs to the already created bug on Bugzilla site." "\nThe bug ID is retrieved from directory specified by -d DIR." "\nIf problem data in DIR was never reported to Bugzilla, upload will fail." "\n" "\nOption -tID uploads FILEs to the bug with specified ID on Bugzilla site." "\n-d DIR is ignored." "\n" "\nOption -w adds bugzilla user to bug's CC list." "\n" "\nOption -r sets the last url from reporter_to element which is prefixed with" "\nTRACKER_NAME to URL field. This option is applied only when a new bug is to be" "\nfiled. The default value is 'ABRT Server'" "\n" "\nIf not specified, CONFFILE defaults to {1}/plugins/bugzilla.conf" "\nand {2}." "\nIts lines should have 'PARAM = VALUE' format." "\nRecognized string parameters: BugzillaURL, BugzillaAPIKey, OSRelease." "\nRecognized boolean parameter (VALUE should be 1/0, yes/no): SSLVerify." "\nUser's local configuration overrides the system wide configuration." "\nParameters can be overridden via $Bugzilla_PARAM environment variables." "\n" "\nFMTFILE and FMTFILE2 default to {1}/plugins/bugzilla_format.conf" ).format(g_progname, const.CONF_DIR, user_conf) program_options = _( "\n --help Print this help and exit" "\n -v, --verbose Be verbose" "\n -d DIR Problem directory" "\n -c FILE Configuration file (may be given many times)" "\n -F FILE Formatting file for initial comment" "\n -A FILE Formatting file for duplicates" "\n -t, --ticket[ID] Attach FILEs [to bug with this ID]" "\n -b When creating bug, attach binary files too" "\n -f Force reporting even if this problem is already reported" "\n -w Add bugzilla user to CC list [of bug with this ID]" "\n -h, --duphash DUPHASH Print BUG_ID which has given DUPHASH" "\n -p, --product[PRODUCT] Specify a Bugzilla product (ignored without -h)" "\n -r, --tracker TRACKER_NAME A name of bug tracker for an additional URL from 'reported_to'" "\n -g, --group GROUP Restrict access to this group only" "\n -D, --debug[STR] Debug\n" ) dump_dir_name = os.getcwd() conf_files = [] fmt_file = os.path.join(const.CONF_DIR, 'plugins/bugzilla_format.conf') fmt_file2 = fmt_file ticket_no = None abrt_hash = None product = None version = None tracker_str = 'ABRT Server' debug = False add_user_to_cc = False rhbz: Dict[str, Any] = {} attach_binaries = False try: opts, args = getopt.getopt(sys.argv[1:], 'vd:c:F:A:t:bfwh:pr:g:D', ['verbose', 'help', 'ticket=', 'duphash=', 'product', 'tracker=', 'group=', 'debug']) except getopt.GetoptError as exc: logger.error(exc) print(program_usage_string) print(program_options) sys.exit(1) for opt, arg in opts: if opt == '--help': print(program_usage_string) print(program_options) sys.exit(0) if opt in ('-v', '--verbose'): g_verbose += 1 elif opt == '-d': dump_dir_name = arg elif opt == '-c': conf_files.append(arg) elif opt == '-F': fmt_file = arg elif opt == '-A': fmt_file2 = arg elif opt in ('-t', '--ticket'): ticket_no = arg elif opt == '-b': attach_binaries = True elif opt == '-w': add_user_to_cc = True elif opt in ('-h', '--duphash'): abrt_hash = arg elif opt in ('-p', '--product'): product = arg if product: continue product = os.environ.get('Bugzilla_Product') if product: continue elif opt in ('-r', '--tracker'): tracker_str = arg elif opt in ('-g', '--group'): rhbz['b_private_group'] = arg elif opt in ('-D', '--debug'): debug = True g_verbose = min(g_verbose, 2) logger_init(logger, ['WARNING', 'INFO', 'DEBUG'][g_verbose]) export_abrt_envvars(0) s_reporter_settings: Dict[str, Any] = {} opt_switches = set(k for k, _ in opts) if '-d' in opt_switches: # pull in some defaults from os-release problem_data_loader = ProblemDataLoader(logger, global_config) problem_data = problem_data_loader.create_problem_data_for_reporting(dump_dir_name) if not problem_data: sys.exit(1) # create_problem_data_for_reporting already emitted error msg os_info = pd_get_item_content(const.FILENAME_OS_INFO, problem_data).split('\n') set_default_settings(os_info, s_reporter_settings) # { conf_file_loader = ConfFileLoader(logger=logger) if not conf_files: conf_files.append(os.path.join(const.CONF_DIR, 'plugins/bugzilla.conf')) conf_files.append(user_conf) for file in conf_files: logger.info("Loading settings from '%s'", file) conf_file_loader.libreport_load_conf_file(file, settings=s_reporter_settings, skip_empty_keys=True) logger.debug("Loaded '%s'", file) set_settings(rhbz, s_reporter_settings) # } # either we got Bugzilla_CreatePrivate from settings or -g was specified on cmdline if opt_switches & {'-g', '--group'}: rhbz['b_create_private'] = True bz_url = s_reporter_settings.get('BugzillaURL') or 'https://bugzilla.redhat.com' ssl_verify = bool('SSLVerify' not in s_reporter_settings or string_to_bool(str(s_reporter_settings['SSLVerify']))) bz_conn = BZConnection(logger, global_config, url=bz_url, verify=ssl_verify) if abrt_hash: logger.warning(_("Looking for similar problems in bugzilla")) if not abrt_hash.startswith('abrt_hash:'): abrt_hash = f'abrt_hash:{abrt_hash}' if opt_switches & {'-p', '--product'}: # If only -p without following string is presented, using # 'REDHAT_BUGZILLA_PRODUCT' value from /etc/os-release or value # from environment variable 'Bugzilla_Product' is used. if not product and not os.environ.get('Bugzilla_Product'): try: with open('/etc/os-release', 'r', encoding='utf-8') as os_release: # Add a dummy section to allow parsing as ini file os_release_content = '[os_release]\n' + os_release.read() config.read_string(os_release_content) product = config['os_release']['REDHAT_BUGZILLA_PRODUCT'].strip('"') if not product: logger.error(_("Failed to get 'REDHAT_BUGZILLA_PRODUCT' " "from '/etc/os-release'.")) except (FileNotFoundError, PermissionError): logger.error(_("Failed to read '/etc/os-release' to get Bugzilla product.")) if not product: # Use DEFAULT_BUGZILLA_PRODUCT as default product due to backward compatibility product = DEFAULT_BUGZILLA_PRODUCT # If parameter -p was used and product == NULL, some error occured if opt_switches & {'-p', '--product'}: logger.error(_("Using default product '%s'"), product) logger.debug("Using Bugzilla product '%s' to find duplicate bug", product) bugs = bz_conn.bug_search( {'quicksearch': f'ALL whiteboard:"{abrt_hash}"'} ) if bugs: print(bugs[0]['id']) sys.exit(0) if not rhbz.get('b_api_key'): try: rhbz['b_api_key'] = ask_password(_("API key is not provided by configuration. Please enter the API key for '{}': ").format(bz_url)) except (EOFError, KeyboardInterrupt): rhbz['b_api_key'] = '' if not rhbz.get('b_api_key'): logger.error(_("Can't continue without API key")) sys.exit(1) try: rhbz.get('b_api_key', '').encode('latin-1') # HTTP headers are encoded with latin-1 (ISO-8859-1) except UnicodeEncodeError as e: logger.error(_("API key contains invalid characters: %s"), str(e)) sys.exit(1) bz_conn.add_api_key(rhbz.get('b_api_key')) if opt_switches & {'-t', '--ticket'}: if ( (not args and '-w' not in opt_switches) or (args and '-w' in opt_switches) ): print(program_usage_string) print(program_options) sys.exit(1) if not ticket_no: dump_dir_obj = DumpDir(logger) dd = dump_dir_obj.dd_opendir(dump_dir_name, 0) reported_to = None url = '' if not dd: # libreport_xfunc_die() sys.exit(1) reported_to = dump_dir_obj.libreport_find_in_reported_to(dd, 'Bugzilla') dump_dir_obj.dd_close(dd) if not reported_to: logger.error(_("Can't get Bugzilla ID because this problem has not yet been reported to Bugzilla.")) sys.exit(1) url = reported_to.get('url') if not url.startswith(rhbz['b_bugzilla_url']): logger.error(_("This problem has been reported to Bugzilla '%s' which differs from the configured Bugzilla '%s'."), url, rhbz['b_bugzilla_url']) if url.rfind('=') == -1: logger.error(_("Malformed url to Bugzilla '%s'."), url) ticket_no = url[url.rfind('=')+1:] logger.warning(_("Using Bugzilla ID '%s'"), ticket_no) if not rhbz['b_bugzilla_url'].endswith('redhat.com'): # Add API key as a REST param, but only for non-RH bugzilla instances bz_conn.add_api_key_param(rhbz['b_api_key']) if '-w' in opt_switches: ticket_intermediate = re.search(r'^\d+', ticket_no) if ticket_intermediate: ticket = ticket_intermediate.group(0) else: logger.error("expected a non-negative integer: '%s'", ticket_no) sys.exit(1) rhbz['b_login'] = ask(_(f"Please enter your {rhbz['b_bugzilla_url']} login:")) if not rhbz['b_login']: logger.error(_("Can't continue without login")) sys.exit(1) update_data = {'ids': [ticket], 'cc': {'add': [rhbz['b_login']]}, 'minor_update': True} bz_conn.bug_update(ticket, update_data) else: # Attach files to existing BZ for filename in args: # anything that isn't a cmdline option logger.info("Attaching file '%s' to bug %s", filename, ticket_no) try: st = os.stat(filename) except OSError: logger.error("Can't open '%s'", filename) continue if not stat.S_ISREG(st.st_mode): logger.error("'%s': not a regular file", filename) continue response = bz_conn.attachment_create(int(ticket_no), filename, False) # TODO: We can't print attachment id because # POST rest/bug/<id>/attachment returns empty response - bug? logger.info("Attached '%s' to bug #%s", filename, ticket_no) sys.exit(0) # Create new bug in Bugzilla if '-f' not in opt_switches: dump_dir_obj = DumpDir(logger) dd = dump_dir_obj.dd_opendir(dump_dir_name, 0) reported_to = None url = '' if not dd: # libreport_xfunc_die(); sys.exit(1) reported_to = dump_dir_obj.libreport_find_in_reported_to(dd, "Bugzilla") dump_dir_obj.dd_close(dd) if reported_to: url = reported_to.get('url') if url: msg = (_(f"This problem was already reported to Bugzilla (see '{url}')." f" Do you still want to create a new bug?")) if not ask_yes_no(msg): sys.exit(0) if '-d' not in opt_switches: problem_data_loader = ProblemDataLoader(logger, global_config) problem_data = problem_data_loader.create_problem_data_for_reporting(dump_dir_name) if not problem_data: sys.exit(1) # create_problem_data_for_reporting already emitted error msg component = pd_get_item_content(const.FILENAME_COMPONENT, problem_data) if not component: sys.exit(1) duphash = pd_get_item_content(const.FILENAME_DUPHASH, problem_data) if not rhbz.get('b_product') or not rhbz.get('b_product_version'): # if not overridden or empty... os_info = pd_get_item_content(const.FILENAME_OS_INFO, problem_data).split('\n') rhbz['b_product'] = os_info_get_value('REDHAT_BUGZILLA_PRODUCT', os_info) rhbz['b_product_version'] = os_info_get_value('REDHAT_BUGZILLA_PRODUCT_VERSION', os_info) if not rhbz['b_product'] or not rhbz['b_product_version']: logger.error(_("Can't determine Bugzilla Product from problem data.")) if opt_switches & {'-D', '--debug'}: pf = ProblemFormatter(fmt_file, problem_data, logger) # errors are handled by ProblemFormatter report = pf.generate_report() if not report: # won't happen. TODO: figure out when report generation should fail logger.error('Failed to format bug report from problem data') print(f"summary: {report['summary']}\n\n{report['text']}", end='') print('attachments:') for attachment in report['attach']: pd_item = pd_get_item(attachment, problem_data) print(f" {pd_item['name']}") sys.exit(0) if not rhbz['b_bugzilla_url'].endswith('redhat.com'): # Add API key as a REST param, but only for non-RH bugzilla instances bz_conn.add_api_key_param(rhbz['b_api_key']) bug_id = 0 # If REMOTE_RESULT contains "DUPLICATE 12345", we consider it a dup of 12345 # and won't search on bz server. remote_result = pd_get_item_content(const.FILENAME_REMOTE_RESULT, problem_data) if remote_result: matches = re.findall(r'DUPLICATE \d+', remote_result) if matches: bug_id = matches[0][matches[0].rfind(' ')+1:] bug_info = None if not bug_id: logger.warning(_('Checking for duplicates')) existing_id = -1 crossver_id = -1 # { # Figure out whether we want to match component # when doing dup search. dont_match_components_str = str(s_reporter_settings.get('DontMatchComponents')) if dont_match_components_str: dont_match_components = [i.strip() for i in dont_match_components_str.split(',')] else: dont_match_components = [] if component in dont_match_components: component_substitute = None else: component_substitute = component # We don't do dup detection across versions (see below why), # but we do add a note if cross-version potential dup exists. # For that, we search for cross version dups first: query = {'quicksearch': f"ALL whiteboard:{duphash} product:{rhbz['b_product']} component:{component_substitute}"} crossver_bugs = bz_conn.bug_search(query) if crossver_bugs: # In dup detection we require match in product *and version*. # Otherwise we sometimes have bugs in e.g. Fedora 17 # considered to be dups of Fedora 16 bugs. # Imagine that F16 is "end-of-lifed" - allowing cross-version # match will make all newly detected crashes DUPed # to a bug in a dead release. crossver_id = crossver_bugs[0]['id'] logger.debug("Bugzilla has %i reports with duphash '%s' including cross-version ones", len(crossver_bugs), duphash) query = {'quicksearch': f"ALL whiteboard:{duphash} product:{rhbz['b_product']} version:{rhbz['b_product_version']} component:{component_substitute}"} dup_bugs = bz_conn.bug_search(query) if dup_bugs: existing_id = dup_bugs[0]['id'] logger.debug("Bugzilla has %i reports with duphash '%s'", len(dup_bugs), duphash) # } if existing_id < 0: pf = ProblemFormatter(fmt_file, problem_data, logger) report = pf.generate_report() # TODO: if not report (how would that happen?) if existing_id > 0: pass # Create new bug logger.warning(_('Creating a new bug...')) if existing_id < 0 <= crossver_id: report['text'] += f"\nPotential duplicate: bug {crossver_id}\n" new_id = bz_conn.bug_create( problem_data, rhbz['b_product'], rhbz['b_product_version'], report['summary'], report['text'], False, None, comment_is_private = rhbz['b_create_private'], extra_private_groups = rhbz['b_private_groups'] ) if new_id == -1: # bug_create logged the error sys.exit(1) attachments_bug_id = new_id if rhbz['b_create_private']: # Create a supplementary private bug for uploading attachments text = _( f"This is a supplementary private bug for uploading attachments for the problem reported in #{new_id}. " f"These attachments are intended to be accessible only to members of the following group(s):\n\n" f" * {",".join(rhbz['b_private_groups'])}" ).format(rhbz['b_private_groups']) if report['summary'].startswith('[abrt]'): # Make the supplementary bugs easily identifiable by adding "[attachments]" to the bug summary report['summary'] = f"[abrt][attachments]{report['summary'][len('[abrt]'):]}" attachments_bug_id = bz_conn.bug_create( problem_data, rhbz['b_product'], rhbz['b_product_version'], report['summary'], text, True, rhbz['b_private_groups'] ) if attachments_bug_id == -1: # bug_create logged the error sys.exit(1) # Immediately close the supplementary private bug with attachments update_data = { 'ids': [attachments_bug_id], 'status': 'CLOSED', 'resolution': 'NOTABUG', 'minor_update': True } bz_conn.bug_update(attachments_bug_id, update_data) dump_dir_obj = DumpDir(logger) dd = dump_dir_obj.dd_opendir(dump_dir_name, 0) if dd: extra = dump_dir_obj.dd_load_text_ext(dd, 'extra-cc', DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE | DD_FAIL_QUIETLY_ENOENT) if extra: emails = extra.split('\n') logger.warning(_("Adding extra ccs to bug report: %s"), emails) update_data = {'ids': [new_id], 'cc': {'add': emails}, 'minor_update': False} bz_conn.bug_update(new_id, update_data) reported_to = dump_dir_obj.libreport_find_in_reported_to(dd, tracker_str) url = '' if reported_to: url = reported_to.get('url') if url: logger.warning(_('Adding External URL to bug %i'), new_id) update_data = {'ids': [new_id], 'url': url, 'minor_update': True} bz_conn.bug_update(new_id, update_data) dump_dir_obj.dd_close(dd) logger.warning(_('Adding attachments to bug %i'), attachments_bug_id) bug_attachments = [] for attachment in report['attach']: response = bz_conn.attachment_create_from_problem_data(attachments_bug_id, attachment, problem_data) logger.info("Attached '%s' to bug no. %s with id %s", attachment, attachments_bug_id, response.json()['ids'][0]) if response.status_code == 201: bug_attachment = { 'name': attachment, 'id': response.json()['ids'][0] } bug_attachments.append(bug_attachment) if rhbz['b_create_private'] and new_id != attachments_bug_id: text = _( "Bugzilla does not offer support for private attachments accessible only to specific groups, " "such as 'fedora_contrib_private'. As a result, a separate private bug has been created, " "and the following attachments have been added there:\n\n" ) for bug_attachment in bug_attachments: # We are hardcoding the URL here. The code already relies on certain "Red Hat Bugzilla API customizations", # so it wouldn't reliably work with any other Bugzilla instance anyway... text += f"* {bug_attachment['name']}: https://bugzilla-attachments.redhat.com/attachment.cgi?id={bug_attachment['id']}\n" # We created a public bug FIXME bz_conn.bug_add_comment( new_id, text, is_private=rhbz['b_create_private'], extra_private_groups=rhbz['b_private_groups'] ) # We just created a new bug, let's make sure that we are in CC bug_info = bz_conn.bug_info(new_id) if (bug_info.get('bi_reporter') and bug_info.get('bi_reporter') not in bug_info['bi_cc_list']): logger.debug(_('Adding %s to CC list'), bug_info.get('bi_reporter')) update_data = {'ids': [bug_info['bi_id']], 'cc': {'add': [bug_info.get('bi_reporter')]}, 'minor_update': True} bz_conn.bug_update(bug_info['bi_id'], update_data) bug_info = {'bi_id': new_id, 'bi_status': 'NEW', 'bi_dup_id': -1} log_out(bug_info, rhbz, dump_dir_name) bug_id = existing_id bug_info = bz_conn.bug_info(bug_id) logger.warning(_("Bug is already reported: %i"), bug_info['bi_id']) # Follow duplicates if bug_info['bi_status'] == 'CLOSED' and bug_info['bi_resolution'] == 'DUPLICATE': origin = bz_conn.find_origin_bug_closed_duplicate(bug_info) if origin: bug_info = origin # Add user's login to CC if not there already, but only if the login is known if ( rhbz.get('b_login') and bug_info['bi_reporter'] != rhbz['b_login'] and rhbz['b_login'] not in bug_info['bi_cc_list'] ): logger.warning(_('Adding %s to CC list'), rhbz['b_login']) update_data = {'ids': [bug_info['bi_id']], 'cc': {'add': [rhbz['b_login']]}, 'minor_update': True} bz_conn.bug_update(bug_info['bi_id'], update_data) # Add comment and bt comment = pd_get_item_content(const.FILENAME_COMMENT, problem_data) if comment: pf = ProblemFormatter(fmt_file2, problem_data, logger) report = pf.generate_report() if not report: # won't happen. TODO: figure out when report generation should fail logger.error('Failed to format duplicate comment from problem data') bzcomment = report.get('text') dup_comment = False trimmed_comment = re.sub(r'[ \t\n]', r'', bzcomment) # We use "or []" here, because the value of "bi_comments" # can be None and NoneType is not iterable. for c in bug_info.get('bi_comments') or []: trimmed_c = re.sub(r'[ \t\n]', r'', c) if trimmed_c == trimmed_comment: dup_comment = True break if not dup_comment: logger.warning(_("Adding new comment to bug %d"), bug_info['bi_id']) bz_conn.bug_add_comment( int(bug_info['bi_id']), bzcomment, is_private = rhbz['b_create_private'], extra_private_groups = rhbz['b_private_groups'] ) else: logger.warning(_('Found the same comment in the bug history, not adding a new one')) log_out(bug_info, rhbz, dump_dir_name)