# configure_bugzilla.py -- Build P4DTI configuration for replication # to Bugzilla. # # Richard Brooksby, Ravenbrook Limited, 2000-12-07 # # $Id: //info.ravenbrook.com/project/p4dti/version/0.4/code/replicator/configure_bugzilla.py#2 $ # # # 1. INTRODUCTION # # This is the automatic configurator for the Bugzilla integration of the # Perforce Defect Tracking Integration (P4DTI). # # This code takes a basic set of configuration parameters [RB 2000-08-10, 5.2] # and generates a full set of parameters for the "dt_bugzilla", "p4", and # "replicator" classes, as well as a Perforce jobspec. # # --- # # Copyright 2000 Ravenbrook Limited. This document is provided "as is", # without any express or implied warranty. In no event will the authors # be held liable for any damages arising from the use of this document. # You may make and distribute copies and derivative works of this # document provided that (1) you do not charge a fee for this document or # for its distribution, and (2) you retain as they appear all copyright # and licence notices and document history entries, and (3) you append # descriptions of your modifications to the document history. import dt_bugzilla import logger import p4 import re import replicator import socket import string import bugzilla import MySQLdb error = "Bugzilla configuration error" # type_table is a map from TeamTrack field type (the value in the FLDTYPE # column in the FIELDS table; see the TeamTrack schema for details) to a tuple # (TeamTrack table name, Perforce field type, Perforce field length, translator # class). The Perforce field length is None when it needs to be the same as # the corresponding field length in TeamTrack. The table name is the table # that auxiliary fields are looked up in. Commented out fields are ones we # don't (yet) support. When new translators are written for those field types, # we can uncomment them. # type_table = { # 100: ( 'NUMERIC', 'word', 32, replicator.translator ), # 101: ( 'TEXT', 'line', None, replicator.translator ), # 102: ( 'MEMO', 'text', 0, dt_teamtrack.text_translator ), # 103: ( 'DATETIME', 'date', 20, dt_teamtrack.date_translator ), # 104: ( 'SELECTION', 'select', 32, dt_teamtrack.single_select_translator ), # #105: ( 'BINARY', 'word', 3, ? ), # 106: ( 'STATE', 'select', 32, dt_teamtrack.state_translator ), # 107: ( 'USER', 'word', 32, dt_teamtrack.user_translator ), # 108: ( 'PROJECTS', 'line', 80, dt_teamtrack.auxiliary_translator ), # #109: ( 'SUMMATION', '?', ?, ? ), # #110: ( 'MULTIPLE_SELECTION', '?', ?, ? ), # #111: ( 'CONTACT', 'word', 32, ? ), # 112: ( 'COMPANIES', 'line', 80, dt_teamtrack.auxiliary_translator ), # #113: ( 'INCIDENT', 'word', 32, ? ), # 114: ( 'PRODUCTS', 'line', 80, dt_teamtrack.auxiliary_translator ), # 115: ( 'SERVICEAGREEMENTS', 'line', 80, dt_teamtrack.auxiliary_translator ), # #116: ( 'FOLDER', 'word', 32, ? ), # #117: ( 'KEYWORDLIST', '?', ?, ? ), # #118: ( 'PRODUCTLIST', '?', ?, ? ), # #119: ( 'PROBLEM', 'word', 32, ? ), # #120: ( 'RESOLUTION', 'word', 32, ? ), # #121: ( 'MERCHANDISE', 'word', 32, ? ), # } # sql_enum_values: columns -> string list. Extract the list of values in # an enum column returned by fetching after a "describe". Doesn't cope # with escaped characters in the enum. def sql_enum_values(column): match = re.match("^enum\('(.*)'\)$", column[1]) assert match != None return string.split(match.group(1), "','") # bugzilla_enum_values: string -> string list. Fetch the list of values for # a particular field from the Bugzilla schema. def bugzilla_enum_values(connection, field_name): connection.execute('describe bugs;') results = connection.fetchall() columns = {} for result in results: columns[result[0]] = result assert columns.has_key(field_name) return sql_enum_values(columns[field_name]) # make_state_pairs: (connection * string) -> (string * string) list. Make a # list of pairs of state names (Bugzilla state, Perforce state). This list # will be used to translate between states, and also to generate the possible # values for the Status field in Perforce. # # The closed_state argument is the Bugzilla state which maps to the special # state 'closed' in Perforce, or None if there is no such state. See # requirement 45. See decision decision [RB 2000-11-28 14:44:36 GMT]. # # The case of state names in these pairs is normalized for usability in # Perforce: see design decision [RB 2000-11-28 14:24:32 GMT]. def make_state_pairs(connection, closed_state): states = bugzilla_enum_values(connection, 'bug_status') state_pairs = [] state_p4_to_dt = {} found_closed_state = 0 for state in states: dt_state = state if closed_state != None and dt_state == closed_state: p4_state = 'closed' found_closed_state = 1 else: p4_state = dt_state # Perforce jobs can't have state "new" (this indicates a fresh job and # Perforce changes the state to "open"). Nor can they have state # "ignore", because that is used in the submit form to indicate that a # job shouldn't be fixed by the change. # # Unfortunately, "new" and "ignore" are common names for states in the # defect tracker (the former is in the default workflow in TeamTrack), # so we don't disallow them, but translate them to "_new" and # "_ignore"; we disallow those instead (or rather, we quit if two # TeamTrack states map to the same state in Perforce.) See job000141. if p4_state in ['new', 'ignore']: p4_state = '_' + p4_state if (state_p4_to_dt.has_key(p4_state) and state_p4_to_dt[p4_state] != dt_state): raise error, ("Two defect tracker states '%s' and '%s' map to the " "same Perforce state '%s'." % (dt_state, state_p4_to_dt[p4_state], p4_state)) state_p4_to_dt[p4_state] = dt_state pair = (dt_state, p4_state) if pair not in state_pairs: state_pairs.append(pair) if closed_state != None and not found_closed_state: raise error, ("You specified the closed_state '%s', but there's no " "such defect tracker state." % closed_state) return state_pairs def configuration(administrator_address = None, changelist_url = None, closed_state = None, log_file = 'p4dti.log', p4_client_executable = 'p4', p4_password = None, p4_port = None, p4_server_description = 'P4DTI Perforce server', p4_user = None, replicated_fields = [], replicator_address = None, rid = 'p4dti', sid = 'p4dti', smtp_server = None, dbms_host = None, dbms_database = None, dbms_port = None, dbms_user = None, dbms_password = '', bugzilla_user = None, ): # Check required arguments. if administrator_address == None: raise error, "You must specify an e-mail address for the resolver." if smtp_server == None: raise error, "You must specify an SMTP server." if dbms_host == None: raise error, "You must specify a MySQL DBMS host and port." if bugzilla_user == None: raise error, "You must specify a Bugzilla user (e-mail address)." # @@@@ Only default fields implemented yet. RB 2000-12-07 if replicated_fields != []: raise error, "replicated_fields not supported by Bugzilla integration yet." # Open a connection to the Bugzilla database. connection = bugzilla.bugzilla(MySQLdb.connect, dbms_host, dbms_port, dbms_database, dbms_user, dbms_password, rid, sid) # Make a list of (Bugzilla state, Perforce state) pairs. state_pairs = make_state_pairs(connection, closed_state) # Make a list of possible resolutions. resolutions = bugzilla_enum_values(connection, 'resolution') # The blank resolution is represented by 'NONE' in Perforce. def nonify(resolution): if resolution == '': return 'NONE' else: return resolution resolutions = map(nonify, resolutions) # @@@@ Need to get the list of fields here, and make sensible descriptions. state_description = "Bugzilla's Status field." owner_description = "Bugzilla's Assigned To field." title_description = "Bugzilla's Summary field." resolution_description = "Bugzilla's Resolution field." # p4_fields maps Bugzilla field name to the jobspec data (number, name, # type, length, dispositon, preset, values, help text, translator). The # five fields 101 to 105 are predefined because they are needed by both # Bugzilla and Perforce and the replicator. The fields Job and Date are # special: they are not replicated from TeamTrack but are required by # Perforce, so we have them here. Note that their help text is given (the # other help texts will be fetched from TeamTrack). p4_fields = { \ '(JOB)': ( 101, 'Job', 'word', 32, 'required', None, None, "The job name.", None ), 'bug_status': ( 102, 'Status', 'select', 32, 'required', state_pairs[0][1], string.join(map((lambda x: x[1]), state_pairs), '/'), state_description, dt_bugzilla.status_translator(state_pairs) ), 'assigned_to': ( 103, 'Assigned_To', 'word', 32, 'required', '$user', None, owner_description, dt_bugzilla.user_translator(bugzilla_user, p4_user) ), '(DATE)': ( 104, 'Date', 'date', 20, 'always', '$now', None, "The date this job was last modified.", None ), 'short_desc': ( 105, 'Summary', 'text', 0, 'required', '$blank', None, title_description, dt_bugzilla.text_translator() ), 'resolution': ( 106, 'Resolution', 'select', 10, 'required', resolutions[0], # @@@@ Could use default from SQL? string.join(resolutions, '/'), resolution_description, dt_bugzilla.resolution_translator() ), '(FILESPECS)': ( 191, 'P4DTI-filespecs', 'text', 0, 'default', None, None, "Associated filespecs. See section ? of the P4DTI " "user guide.", None ), '(ACTION)': ( 192, 'P4DTI-action', 'select', 32, 'required', 'replicate', 'keep/discard/wait/replicate', "Replicator action. See section 11 of the P4DTI " "administrator guide.", None ), '(RID)': ( 193, 'P4DTI-rid', 'word', 32, 'required', 'None', None, "P4DTI replicator identifier. Do not edit!", None ), '(ISSUE)': ( 194, 'P4DTI-issue-id', 'word', 32, 'required', 'None', None, "Bugzilla issue database identifier. Do not edit!", None ), '(USER)': ( 195, 'P4DTI-user', 'word', 32, 'always', '$user', None, "Last user to edit this job. You can't edit this!", None ), } # The other replicated fields will be sequential from this field id. p4_field_id = 110 # Go through the replicated_fields list, build structures and add them to # p4_fields. # for dt_field_name in replicated_fields: # # Convert the field name to uppercase. # dt_field_name = string.upper(dt_field_name) # # # Field must exist in TeamTrack. # if not dt_fields.has_key(dt_field_name): # raise error, ("Field '%s' specified in 'replicated_fields' list " # "not in TeamTrack FIELDS table." % dt_field_name) # # # Field must not be already in the p4_fields structure. # if p4_fields.has_key(dt_field_name): # if dt_field_name in ['STATE', 'OWNER', 'TITLE']: # raise error, ("Field '%s' specified in 'replicated_fields' " # "list is a system field: leave it out!" # % dt_field_name) # else: # raise error, ("Field '%s' appears twice in " # "'replicated_fields'." % dt_field_name) # # f = dt_fields[dt_field_name] # dt_field_type = f['FLDTYPE'] # # # TeamTrack variable length text fields are often (always?) not given # # as MEMO fields (type 102) but as TEXT fields (type 101) with # # ATTRIBUTES set to 0 to indicate that it is variable-length. So # # handle that special case here. # if dt_field_type == 101 and f['ATTRIBUTES'] == 0: # dt_field_type = 102 # # # Look up the field type in type_table to find out how to convert it to # # a Perforce field. # if not type_table.has_key(dt_field_type): # raise error, ("Field '%s' has type %d: this is not supported by " # "P4DTI." % (dt_field_name, dt_field_type)) # dt_table_name, p4_field_type, p4_field_length, translator_class = \ # type_table[dt_field_type] # p4_field_values = None # p4_field_preset = None # p4_field_name = dt_teamtrack.translate_keyword_dt_to_p4(f['NAME']) # # "p4 -G" uses the field "code" to indicate whether the Perforce # # command succeeded or failed. See job000003. # if p4_field_name == 'code': # raise error, ("You can't have a field called 'code' in the " # "Perforce jobspec.") # # # Fixed-length text fields get the length from TeamTrack. # if p4_field_length == None: # p4_field_length = f['LEN'] # # # Work out the arguments to pass to the translator constructor, and # # apply any other special cases. # translator_arguments = () # if translator_class == dt_teamtrack.single_select_translator: # # For single-select fields, get the list of values from TeamTrack. # selections = connection.query(teamtrack.table['SELECTIONS'], # "TS_FLDID = %d" % f['ID']) # p4_field_values = string.join(map(lambda(s): # dt_teamtrack.translate_keyword_dt_to_p4(s['NAME']), selections) + ['(None)'], '/') # translator_arguments = (f['DBNAME'],) # elif translator_class == dt_teamtrack.state_translator: # translator_arguments = (state_pairs,) # elif translator_class == dt_teamtrack.auxiliary_translator: # translator_arguments = (dt_table_name,) # # p4_fields[dt_field_name] = \ # ( p4_field_id, # p4_field_name, # p4_field_type, # p4_field_length, # 'optional', # p4_field_preset, # p4_field_values, # f['DESCRIPTION'], # apply(translator_class,translator_arguments), # ) # p4_field_id = p4_field_id + 1 # if p4_field_id >= 191: # raise error, ("Too many fields to replicate: Perforce jobs can " # "contain only 99 fields.") # Build a jobspec, suitable for "p4 -G jobspec -i". jobspec = {} p4_fields_list = p4_fields.values() # This works in Python 1.5.2, but it doesn't follow the manual, which says # that the comparison function must return -1, 0, or 1. What about lexical # sorting? p4_fields_list.sort(lambda x,y: x[0] - y[0]) i = 0 for field in p4_fields_list: jobspec['Fields%d' % i] = "%s %s %s %s %s" % field[0:5] i = i + 1 i = 0 for field in p4_fields_list: if field[6] != None: jobspec['Values%d' % i] = "%s %s" % (field[1], field[6]) i = i + 1 i = 0 for field in p4_fields_list: if field[5] != None: jobspec['Presets%d' % i] = "%s %s" % (field[1], field[5]) i = i + 1 def comment(field): return "# %s: %s\n" % (field[1], field[7]) jobspec['Comments'] = ("# DO NOT CREATE NEW JOBS IN PERFORCE. " "USE THE DEFECT TRACKER.\n#\n" + string.join(map(comment, p4_fields.values()), "")) # The logger object that handles the logging. This logger object logs both # to the file and to stdout. logger_object = logger.multi_logger([logger.file_logger(open(log_file, "a")), logger.file_logger()]) # Build Bugzilla and replicator configuration dictionaries. bugzilla_config = \ { 'changelist-url': changelist_url, 'logger': logger_object, 'db' : MySQLdb.connect, 'dbms-host': dbms_host, 'dbms-database': dbms_database, 'dbms-port': dbms_port, 'dbms-user': dbms_user, 'dbms-password': dbms_password, 'jobname-function': lambda bug: 'bug%d' % bug['bug_id'] } # configured_fields is a list of triples (Bugzilla database field name, # Perforce field name, translator) required by the replicator. We use the # filter to remove the fields that aren't replicated: these have no # translator. configured_fields = \ map(lambda item: (item[0], item[1][1], item[1][8]), filter(lambda item: item[1][8] != None, p4_fields.items())) p4_config = \ { 'p4-client': 'p4dti-%s' % socket.gethostname(), 'p4-client-executable': p4_client_executable, 'p4-password': p4_password, 'p4-port': p4_port, 'p4-user': p4_user, } replicator_config = \ { 'administrator-address': administrator_address, 'date-translator': dt_bugzilla.date_translator(), 'job-owner-field': 'User', 'job-status-field': 'Status', 'logger': logger_object, 'replicated-fields': configured_fields, 'replicator-address': replicator_address, 'smtp-server': smtp_server, 'user-translator': dt_bugzilla.user_translator(bugzilla_user, p4_user), } return bugzilla_config, p4_config, replicator_config, jobspec # A. REFERENCES # # [RB 2000-08-10] "Perforce Defect Tracking Integration Administrator's # Guide"; Richard Brooksby; Ravenbrook Limited; 2000-08-10. # # # B. DOCUMENT HISTORY # # 2000-12-07 RB Branched and adapted from configure_teamtrack.py. # # 2000-12-08 GDR Translate state name "ignore" to "_ignore".