def link_bug_to_site(bug_key: str, site_key: str) -> None: print("Linking bug '{}' with site '{}".format(bug_key, site_key)) jira = JIRA(settings.JIRA_URL, basic_auth=(settings.JIRA_USERNAME, settings.JIRA_PASSWORD)) jira.create_issue_link( type="blocks", inwardIssue=bug_key, outwardIssue=site_key, comment={ "body": "Linking '%s' --> '%s'" % (bug_key, site_key), } )
class JiraClient: """Wrapper around Jira client.""" def __init__(self, jira_board: Mapping[str, Any], settings: Optional[Mapping] = None): self.secret_reader = SecretReader(settings=settings) self.project = jira_board["name"] jira_server = jira_board["server"] self.server = jira_server["serverUrl"] token = jira_server["token"] token_auth = self.secret_reader.read(token) self.jira = JIRA(self.server, token_auth=token_auth) def get_issues(self, fields: Optional[Mapping] = None) -> GottenIssue: block_size = 100 block_num = 0 all_issues: GottenIssue = [] jql = "project={}".format(self.project) kwargs: dict[str, Any] = {} if fields: kwargs["fields"] = ",".join(fields) while True: index = block_num * block_size issues = self.jira.search_issues(jql, index, block_size, **kwargs) all_issues.extend(issues) if len(issues) < block_size: break block_num += 1 return all_issues def create_issue( self, summary: str, body: str, labels: Optional[Iterable[str]] = None, links: Iterable[str] = (), ) -> Issue: """Create an issue in our project with the given labels.""" issue = self.jira.create_issue( project=self.project, summary=summary, description=body, labels=labels, issuetype={"name": "Task"}, ) for ln in links: self.jira.create_issue_link(type="is caused by", inwardIssue=issue.key, outwardIssue=ln) return issue
def _link_jiras( client: jira.JIRA, from_jira: str, to_jira: str, relation_type: str = DEFAULT_LINK_TYPE, ) -> requests.Response: return client.create_issue_link(relation_type, from_jira, to_jira)
class Jira: # {{{ Constants BLOCKS = 'Blocks' DUPLICATE = 'Duplicate' RELATES = 'Relates' # }}} # {{{ init(address) - Initialise JIRA class, pointing it to the JIRA endpoint def __init__(self, address='https://r3-cev.atlassian.net'): self.address = address self.jira = None self.mock_key = 1 self.custom_fields_by_name, self.custom_fields_by_key = {}, {} # }}} # {{{ login(user, password) - Log in as a specific JIRA user def login(self, user, password): try: self.jira = JIRA(self.address, auth=(user, password)) for x in self.jira.fields(): if x['custom']: self.custom_fields_by_name[x['name']] = x['key'] self.custom_fields_by_key[x['key']] = x['name'] return self except Exception as error: message = error.message if isinstance(error, JIRAError): message = error.text if error.text and len( error.text) > 0 and not error.text.startswith( '<!') else message raise Exception('failed to log in to JIRA{}{}'.format( ': ' if message else '', message)) # }}} # {{{ search(query) - Search for issues and manually traverse pages if multiple pages are returned def search(self, query, *args): max_count = 50 index, offset, count = 0, 0, max_count query = query.format(*args) if len(args) > 0 else query while count == max_count: try: issues = self.jira.search_issues(query, maxResults=max_count, startAt=offset) count = len(issues) offset += count for issue in issues: index += 1 yield Issue(self, index=index, issue=issue) except JIRAError as error: raise Exception('failed to run query "{}": {}'.format( query, error.text)) # }}} # {{{ find(key) - Look up issue by key def find(self, key): try: issue = self.jira.issue(key) return Issue(self, issue=issue) except JIRAError as error: raise Exception('failed to look up issue "{}": {}'.format( key, error.text)) # }}} # {{{ create(fields, dry_run) - Create a new issue def create(self, fields, dry_run=False): if dry_run: return Issue(self, fields=fields) try: fields['labels'] = filter(lambda x: x is not None, fields['labels']) issue = self.jira.create_issue(fields) return Issue(self, issue=issue) except JIRAError as error: raise Exception('failed to create issue: {}'.format(error.text)) # }}} # {{{ link(issue_key, other_issue_key, relationship, dry_run) - Link one issue to another def link(self, issue_key, other_issue_key, relationship=RELATES, dry_run=False): if dry_run: return try: self.jira.create_issue_link(type=relationship, inwardIssue=issue_key, outwardIssue=other_issue_key, comment={ 'body': 'Linked {} to {}'.format( issue_key, other_issue_key), }) except JIRAError as error: raise Exception('failed to link {} and {}: {}'.format( issue_key, other_issue_key, error.text))
def main(credential_file, child_issue, parent_issue, link_type): rest_url_file = DEFAULT_URL_FILE if not os.path.exists(rest_url_file): print("JIRA REST URL file '{}' does not exist".format(rest_url_file)) sys.exit(1) else: with open(rest_url_file, 'r') as f: url = f.readline() url = url.strip() print("read the REST URL from file '{}'".format(rest_url_file)) if credential_file is None: credential_file = DEFAULT_CREDENTIAL_FILE if not os.path.exists(credential_file): print( "JIRA credential file '{}' does not exist".format(credential_file)) sys.exit(1) error_ctr = 0 if child_issue is None: print("--child_issue was not specified") error_ctr += 1 if parent_issue is None: print("--parent_issue was not specified") error_ctr += 1 if error_ctr > 0: print("Required parameter(s) not defined") sys.exit(1) if link_type is None: link_type = DEFAULT_LINK_TYPE print( "--link_type was not specified and therefore was set to default '{}'" .format(link_type)) with open(credential_file, 'r') as f: line = f.readline() line = line.strip() (username, password) = line.split(':') print("read username and password from credentials file '{}'".format( credential_file)) auth_jira = JIRA(url, basic_auth=(username, password)) if auth_jira is None: print("Could not instantiate JIRA for url '{}'".format(url)) sys.exit(1) print("Will attempt to link JIRA issue '{}' to '{}' with link type '{}'". format(child_issue, parent_issue, link_type)) try: auth_jira.create_issue_link(type=link_type, inwardIssue=child_issue, outwardIssue=parent_issue, comment={ "body": "Linking {} to {}".format( child_issue, parent_issue) }) except Error as e: print( "Encountered some exception while attempting to link '{}' to '{}' with link type '{}': {}" .format(child_issue, parent_issue, link_type, e)) sys.exit(1) else: print("Linked '{}' to '{}' with link type '{}'".format( child_issue, parent_issue, link_type))
class Thread(QThread): '''Threaded process to send data to jira''' add_log_post = pyqtSignal(str) set_progress_bar = pyqtSignal(int, int) def __init__(self, task_list, config): self.config = config self.task_list = task_list self.options = {'server': self.config["jira_base_url"]} self.jira = "" QThread.__init__(self) def stop(self): ''' kills all threads ''' self.terminate() def run(self): ''' main process that reads task list, connects to jira, and creates every DEV and QA task''' if not self.config['project_key']: self.add_log_post.emit("Por favor defina uma chave de projeto") self.stop() self.add_log_post.emit(u'Conectando no Jira...') self.jira = JIRA(self.options, basic_auth=(self.config['jira_user'], self.config['jira_token'])) for task in self.task_list: last_dev_key = None if isinstance(task["devpts"], (int, float)) and task["devpts"] >= 0: self.add_log_post.emit("Adding Dev Task: ") self.add_log_post.emit("+-- Parent: {}".format(task["parent"])) self.add_log_post.emit("+-- Title : {}".format(task["title"])) self.add_log_post.emit("+-- Points: {}".format(task["devpts"])) self.add_log_post.emit("+-- Descri: {}".format(task["desc"])) rootnn_dict = { 'project': { 'key': self.config["project_key"] }, 'summary': "DEV - {}".format(str(task["title"])), 'description': str(task["desc"]), 'customfield_10004': int(task["devpts"]), 'issuetype': { 'name': 'Development Task' }, 'parent': { 'key': str(task["parent"]) } } try: print("dev") print(rootnn_dict) child = self.jira.create_issue(fields=rootnn_dict) last_dev_key = child.key self.add_log_post.emit("+-- SubTask Key: {}".format( child.key)) except Exception as error: self.add_log_post.emit("{}".format(error)) self.stop() self.add_log_post.emit(" ") # QA Task if isinstance(task["qapts"], (int, float)) and task["qapts"] >= 0: self.add_log_post.emit("Adding QA Task: ") self.add_log_post.emit("+-- Parent: {}".format(task["parent"])) self.add_log_post.emit("+-- Title : {}".format(task["title"])) self.add_log_post.emit("+-- Points: {}".format(task["qapts"])) self.add_log_post.emit("+-- Descri: {}".format(task["desc"])) rootnn_dict = { 'project': { 'key': self.config["project_key"] }, 'summary': "QA - {}".format(str(task["title"])), 'description': str(task["desc"]), 'customfield_10004': int(task["qapts"]), 'issuetype': { 'name': 'QA Task' }, 'parent': { 'key': str(task["parent"]) } } try: print("qa") child = self.jira.create_issue(fields=rootnn_dict) self.add_log_post.emit("+-- SubTask Key: {}".format( child.key)) except Exception as error: self.add_log_post("{}".format(error)) if last_dev_key: try: self.jira.create_issue_link(type="depended by", inwardIssue=child.key, outwardIssue=last_dev_key) except Exception as error: self.add_log_post("{}".format(error)) self.add_log_post.emit(" ")
} jira = JIRA(basic_auth=('login', 'pass'), options={'server': 'http://example.com'}) for issue in jira.search_issues('Your JQL here'): #print('{}: {}: {}: {}'.format(issue.key, issue.fields.summary, issue.fields.customfield_10002, issue.fields.components)) existingComponents = [] for component in issue.fields.components: existingComponents.append({"name": component.name}) #print existingComponents existingVersion = [] for version in issue.fields.fixVersions: existingVersion.append({"name": version.name}) #print existingVersion issue_dict['summary'] = issue.fields.summary issue_dict['customfield_10002'] = issue.fields.customfield_10002 issue_dict['components'] = existingComponents issue_dict['priority'] = { 'id': issue.fields.priority.id, 'name': issue.fields.priority.name } issue_dict['fixVersions'] = existingVersion print issue_dict new_issue = jira.create_issue(fields=issue_dict) #link issues, if need it jira.create_issue_link(type="includes", inwardIssue=new_issue.key, outwardIssue=issue.key) print(new_issue.key)
class AugurJira(object): """ A thin wrapper around the Jira module providing some refinement for things like fields, convenience methods for ticket actions and awareness for Augur-specific data types. """ jira = None def __init__(self, server=None, username=None, password=None): self.logger = logging.getLogger("augurjira") self.server = server or settings.main.integrations.jira.instance self.username = username or settings.main.integrations.jira.username self.password = password or settings.main.integrations.jira.password self.fields = None self.jira = JIRA(basic_auth=(self.username, self.password), server=self.server, options={"agile_rest_path": "agile"}) self._field_map = {} self._default_fields = munchify({ "summary": None, "description": None, "status": None, "priority": None, "parent": None, "resolution": None, "epic link": None, "dev team": None, "labels": None, "issuelinks": None, "development": None, "reporter": None, "assignee": None, "issuetype": None, "project": None, "creator": None, "attachment": None, "worklog": None, "story points": None, "changelog": None }) self.fields = api.get_memory_cached_data('custom_fields') if not self.fields: fields = api.memory_cache_data(self.jira.fields(), 'custom_fields') self.fields = {f['name'].lower(): munchify(f) for f in fields} default_fields = {} for df, val in self._default_fields.items(): default_fields[df] = self.get_field_by_name(df) self._default_fields = munchify(default_fields) @property def default_fields(self): """ Returns a dict containing the friendly name of fields as keys and jira's proper field names as values. :return: dict """ return self._default_fields def get_field_by_name(self, name): """ Returns the true field name of a jira field based on its friendly name :param name: The friendly name of the field :return: A string with the true name of a field. """ assert self.fields try: _name = name.lower() if _name.lower() in self.fields: return self.fields[_name]['id'] else: return name except (KeyError, ValueError): return name def link_issues(self, link_type, inward, outward, comment=None): """ Establishes a link in jira between two issues :param link_type: A string indicating the relationship from the inward to the outward (Example: "is part of this release") :param inward: Can be one of: Issue object, Issue dict, Issue key string :param outward: Can be one of: Issue object, Issue dict, Issue key string :param comment: None or a string with the comment associated with the link :return: No return value. """ "" if isinstance(inward, dict): inward_key = inward['key'] elif isinstance(inward, Issue): inward_key = inward.key elif isinstance(inward, str): inward_key = inward else: raise TypeError("'inward' parameter is not of a valid type") if isinstance(outward, dict): outward_key = outward['key'] elif isinstance(outward, Issue): outward_key = outward.key elif isinstance(outward, str): outward_key = outward else: raise TypeError("'outward' parameter is not of a valid type") self.jira.create_issue_link(link_type, inward_key, outward_key, comment) def create_ticket(self, create_fields, update_fields=None, watchers=None): """ Create the ticket with the required fields above. The other keyword arguments can be used for other fields although the values must be in the correct format. :param update_fields: :param create_fields: All fields to include in the creation of the ticket. Keys include: project: A string with project key name (required) issuetype: A dictionary containing issuetype info (see Jira API docs) (required) summary: A string (required) description: A string :param update_fields: A dictionary containing reporter info (see Jira API docs) :param watchers: A list of usernames that will be added to the watch list. :return: Return an Issue object or None if failed. """ try: ticket = self.jira.create_issue(create_fields) if ticket: try: # now update the remaining values (if any) # we can't do this earlier because assignee and reporter can't be set during creation. if update_fields and len(update_fields) > 0: ticket.update(update_fields) except Exception as e: self.logger.warning( "Ticket was created but not updated due to exception: %s" % e.message) try: if watchers and isinstance(watchers, (list, tuple)): [self.jira.add_watcher(ticket, w) for w in watchers] except Exception as e: self.logger.warning( "Unable to add watcher(s) due to exception: %s" % e.message) return ticket except Exception as e: self.logger.error("Failed to create ticket: %s", e.message) return None
class jira_client(object): """Simple wrapper around jira api for build baron analyzer needs""" def __init__(self, jira_server, jira_user): self.jira = JIRA( options={'server': jira_server, 'verify': False}, basic_auth=(jira_user, jira_client._get_password(jira_server, jira_user)), validate=True) # Since the web server may share this client among threads, use a lock since it unclear if # the JIRA client is thread-safe self._lock = threading.Lock() @staticmethod def _get_password(server, user): global keyring password = None if keyring: try: password = keyring.get_password(server, user) except: print("Failed to get password from keyring") keyring = None if password is not None: print("Using password from system keyring.") else: password = getpass.getpass("Jira Password:"******"Store password in system keyring? (y/N): ").strip() if answer == "y": keyring.set_password(server, user, password) return password def query_duplicates_text(self, fields): search = "project in (bf, server, evg, build) AND (" + " or ".join( ['text~"%s"' % f for f in fields]) + ")" return search def search_issues(self, query, maxResults=50): with self._lock: results = self.jira.search_issues( query, fields=[ "id", "key", "status", "resolution", "summary", "created", "updated", "assignee", "description" ], maxResults=maxResults) print("Found %d results" % len(results)) return results def add_affected_version(self, bf_issue, affected_version_string): """ Adds a new 'affectedVersion' to a BF, or does nothing if the version is already present in the issue's 'affectedVersions'. """ affected_versions = bf_issue.fields.versions if affected_versions is None: affected_versions = [] else: affected_versions = [{"name": v.name} for v in affected_versions] if affected_version_string.lower() == "master": affected_version_string = "3.6" if {"name": affected_version_string} in affected_versions: return affected_versions.append({"name": affected_version_string}) try: bf_issue.update(fields={"versions": affected_versions}) except JIRAError as e: print("Error updating issue's affected versions: " + str(e)) def add_failing_task(self, bf_issue, failing_task_string): """ Adds a new 'failing_task' to a BF, or does nothing if the version is already present in the issue's 'failing_task's. """ failing_tasks = bf_issue.fields.customfield_12950 if failing_tasks is None: failing_tasks = [] if failing_task_string in failing_tasks: return failing_tasks.append(failing_task_string) try: bf_issue.update(fields={"customfield_12950": failing_tasks}) except JIRAError as e: print("Error updating duplicate issue's failing_tasks: " + str(e)) def add_affected_variant(self, bf_issue, variant_string): """ Adds a new 'Buildvariant' to a BF, or does nothing if the version is already present in the issue's 'Buildvariant's, or if 'variant_string' is not a valid Buildvariant specifier. The latter case can happen if it is the name of a variant on an old branch, or if it is a new variant and we haven't updated JIRA yet. """ affected_variants = bf_issue.fields.customfield_11454 if affected_variants is None: affected_variants = [] else: affected_variants = [{"value": v.value} for v in affected_variants] if {"value": variant_string} in affected_variants: return affected_variants.append({"value": variant_string}) try: bf_issue.update(fields={"customfield_11454": affected_variants}) except JIRAError as e: print("Error updating duplicate issue's Buildvariants: " + str(e)) def add_github_backtrace_context(self, issue_string, backtrace): """ Given a backtrace represented as follows, appends some nicely formatted previews of the code involved in the backtrace and adds them to the issue's description. A backtrace is a list of frames, specified as follows: { "github_url": "https://github.com/mongodb/mongo/blob/deadbeef/jstests/core/test.js#L42", "first_line_number": 37, "line_number": 42, "frame_number": 0, "file_path": "jstests/core/test.js", "file_name": "test.js", "lines": ["line 37", "line 38", ..., "line 47"] } """ new_description_lines = [""] for frame in backtrace: frame_title = "Frame {frame_number}: [{path}:{line_number}|{url}]".format( frame_number=frame["frame_number"], path=frame["file_path"], line_number=frame["line_number"], url=frame["github_url"] ) # raw text is 0-based, code lines are 1-based. first_line = frame["first_line_number"] + 1 code_block_header = ( "{code:js|title=%s|linenumbers=true|firstline=%d|highlight=%d}" % (frame["file_name"], first_line, frame["line_number"]) ) new_description_lines.append("") new_description_lines.append(frame_title) new_description_lines.append(code_block_header) new_description_lines.extend(frame["lines"]) new_description_lines.append("{code}") self.append_to_issue_description(issue_string, new_description_lines) def add_fault_comment(self, issue_string, fault): """ Adds a {noformat} block to the jira ticket's description summarizing the fault that we have extracted from the logs. """ self.append_to_issue_description( issue_string, [ "", "Extracted {fault_type}: ".format(fault_type=fault.category), "{noformat}", fault.context, "{noformat}" ] ) def append_to_issue_description(self, issue_string, new_lines): """ Adds the given text to the bottom of the given issue's description. If the ticket has already been tagged with the 'bot-analyzed' tag, no update occurs. """ jira_issue = self.get_bfg_issue(issue_string) try: if "bot-analyzed" not in jira_issue.fields.labels: print("Updating issue description: \n" + "\n".join(new_lines)) jira_issue.update( description=jira_issue.fields.description + "\n".join(new_lines)) else: print("Skipping issue update, since issue is already tagged with 'bot-analyzed'") except JIRAError as e: print("Error updating JIRA: " + str(e)) # jira.resolutions() # <JIRA Resolution: name='Fixed', id='1'> # <JIRA Resolution: name="Won't Fix", id='2'> # <JIRA Resolution: name='Duplicate', id='3'> # <JIRA Resolution: name='Incomplete', id='4'> # <JIRA Resolution: name='Cannot Reproduce', id='5'> # <JIRA Resolution: name='Works as Designed', id='6'> # <JIRA Resolution: name='Gone away', id='7'> # <JIRA Resolution: name='Community Answered', id='8'> # <JIRA Resolution: name='Done', id='9'> # Issue link Types # <JIRA IssueLinkType: name='Backports', id='10420'> # <JIRA IssueLinkType: name='Depends', id='10011'> # <JIRA IssueLinkType: name='Documented', id='10320'> # <JIRA IssueLinkType: name='Duplicate', id='10010'> # <JIRA IssueLinkType: name='Gantt Dependency', id='10020'> # <JIRA IssueLinkType: name='Gantt End to End', id='10423'> # <JIRA IssueLinkType: name='Gantt End to Start', id='10421'> # <JIRA IssueLinkType: name='Gantt Start to End', id='10424'> # <JIRA IssueLinkType: name='Gantt Start to Start', id='10422'> # <JIRA IssueLinkType: name='Related', id='10012'> # <JIRA IssueLinkType: name='Tested', id='10220'> def close_as_duplicate(self, issue, duplicate_issue): with self._lock: src_issue = self.jira.issue(issue) dest_issue = self.jira.issue(duplicate_issue) # Add duplicate link self.jira.create_issue_link( type='Duplicate', inwardIssue=issue, outwardIssue=duplicate_issue) # Update affectsVersions, Buildvariants, etc. title_parsing_regex = re.compile("(Timed Out|Failures?):" " (?P<suite_name>.*?)" " on" " (?P<variant_prefix>[^\(\[]+)" "(?P<variant_suffix>" " (?:" "\(Clang 3\.7/libc\+\+\)|" "\(No Journal\)|" "\(inMemory\)|" "\(ephemeralForTest\)|" "\(Unoptimized\))(?: DEBUG)?)?" " (?:\(" "(?P<test_names>(.*?(\.js|CheckReplDBHash)(, )?)*)" "\))? ?\[MongoDB \(" "(?P<version>.*?)" "\) @ [0-9A-Za-z]+\]") parsed_title = title_parsing_regex.match(src_issue.fields.summary) if parsed_title is not None: # Update the failing variants. variant = parsed_title.group("variant_prefix").rstrip() if parsed_title.group("variant_suffix") is not None: variant += parsed_title.group("variant_suffix").rstrip() self.add_affected_variant(dest_issue, variant) # Update the failing tasks. self.add_failing_task(dest_issue, parsed_title.group("suite_name")) # Update the affected versions. self.add_affected_version(dest_issue, parsed_title.group("version")) # Close - id 2 # Duplicate issue is 3 self.jira.transition_issue(src_issue, '2', resolution={'id': '3'}) mongo_client.remove_issue(issue) def close_as_goneaway(self, issue): with self._lock: src_issue = self.jira.issue(issue) # Close - id 2 # Gone away is 7 self.jira.transition_issue( src_issue, '2', comment="Transient machine issue.", resolution={'id': '7'}) mongo_client.remove_issue(issue) def get_bfg_issue(self, issue_number): if not issue_number.startswith("BFG-") and not issue_number.startswith("BF-"): issue_number = "BFG-" + issue_number with self._lock: src_issue = self.jira.issue(issue_number) return src_issue
def create_bug_issue(self, channel, summary, description, component, version, labels, attachments={}, user=JIRA_USER, passwd=JIRA_PASS, project=JIRA_PROJECT, DRY_RUN=False): """ Creates a bug issue on Jira :param channel: The channel to notify :param summary: The title summary :param description: Description field :param component: Component bug affects :param version: Version this bug affects :param labels: Labels to attach to the issue :param user: User to report bug as :param passwd: Password :param project: Jira project """ def add_attachments(jira, ticketId, attachments): for file in attachments: urlretrieve(attachments[file], file) jira.add_attachment(ticketId, os.getcwd() + '/' + file, file) os.unlink(file) if user and passwd and project: try: jira = JIRA(server='https://issues.voltdb.com/', basic_auth=(user, passwd), options=dict(verify=False)) except: self.logger.exception('Could not connect to Jira') return else: self.logger.error( 'Did not provide either a Jira user, a Jira password or a Jira project' ) return # Check for existing bugs for the same test case, if there are any, suppress filing another test_case = summary.split(' ')[0] existing = jira.search_issues( 'summary ~ \'%s\' and labels = automatic and status != Closed' % test_case) if len(existing) > 0: self.logger.info('Found open issue(s) for "' + test_case + '" ' + ' '.join([k.key for k in existing])) # Check if new failure is on different job than existing ticket, if so comments job = summary.split()[-2] existing_ticket = jira.issue(existing[0].id) if job not in existing_ticket.fields.summary: comments = jira.comments(existing[0].id) for comment in comments: # Check for existing comment for same job, if there are any, suppress commenting another if job in comment.body: self.logger.info('Found existing comment(s) for "' + job + '" on open issue') return self.logger.info( 'Commenting about separate job failure for %s on open issue' % test_case) if not DRY_RUN: jira.add_comment(existing[0].id, summary + '\n\n' + description) add_attachments(jira, existing[0].id, attachments) return issue_dict = { 'project': project, 'summary': summary, 'description': description, 'issuetype': { 'name': 'Bug' }, 'labels': labels } jira_component = None components = jira.project_components(project) for c in components: if c.name == component: jira_component = {'name': c.name, 'id': c.id} break if jira_component: issue_dict['components'] = [jira_component] else: # Components is still a required field issue_dict['components'] = ['Core'] jira_version = None versions = jira.project_versions(project) version = 'V' + version for v in versions: if str(v.name) == version.strip(): jira_version = {'name': v.name, 'id': v.id} break if jira_version: issue_dict['versions'] = [jira_version] else: # Versions is still a required field issue_dict['versions'] = ['DEPLOY-Integration'] issue_dict['fixVersions'] = [{'name': 'Backlog'}] issue_dict['priority'] = {'name': 'Blocker'} self.logger.info("Filing ticket: %s" % summary) if not DRY_RUN: new_issue = jira.create_issue(fields=issue_dict) add_attachments(jira, new_issue.id, attachments) #self.logger.info('NEW: Reported issue with summary "' + summary + '"') if self.connect_to_slack(): self.post_message( channel, 'Opened issue at https://issues.voltdb.com/browse/' + new_issue.key) suite = summary.split('.')[-3] # Find all tickets within same test suite and link them link_tickets = jira.search_issues( 'summary ~ \'%s\' and labels = automatic and status != Closed and reporter in (voltdbci)' % suite) for ticket in link_tickets: jira.create_issue_link('Related', new_issue.key, ticket) else: new_issue = None return new_issue
def main(): """creates tickets for a release task""" parser = argparse.ArgumentParser( description='Creates tickets for release certification') parser.add_argument('-u', '--username', help='jira username', default='admin') parser.add_argument('-p', '--password', help='jira password', default='admin') parser.add_argument('-c', '--config', help='path to config file', default='./options.ini') parser.add_argument('-j', '--jira', help='url of jira server', default='http://localhost:8080') args = parser.parse_args() jira_user = args.username jira_pass = args.password jira_server = args.jira config_file_path = args.config CONFIG.read(config_file_path) parent_ticket = config_map('JiraOptions')['parent_ticket'] apprenda_version = config_map('VersionInfo')['to_version'] jira_project = config_map('JiraOptions')['project'] jira_issue_type = config_map('JiraOptions')['issue_type'] jira = JIRA(jira_server, basic_auth=(jira_user, jira_pass)) parent_issue = jira.issue(parent_ticket) ticket_list = [] # create clean install tickets clean_strings = config_map('CleanInstallSection') for cloud in ['single', 'hybrid']: ticket_to_add = ticket.Ticket(jira_project, jira_issue_type) ticket_to_add.format_summary(clean_strings['summary'], apprenda_version, cloud) ticket_to_add.format_description(clean_strings['description']) ticket_list.append(ticket_to_add.__dict__) # create upgrade tickets from_versions = json.loads(config_map('VersionInfo')['from_versions']) upgrade_strings = config_map('UpgradeSection') # single cloud for version in from_versions: ticket_to_add = ticket.Ticket(jira_project, jira_issue_type) ticket_to_add.format_summary(upgrade_strings['summary'], apprenda_version, version, "single") ticket_to_add.format_description(upgrade_strings['description']) ticket_list.append(ticket_to_add.__dict__) # hybrid cloud for version in from_versions: ticket_to_add = ticket.Ticket(jira_project, jira_issue_type) ticket_to_add.format_summary(upgrade_strings['summary'], apprenda_version, version, "hybrid") ticket_to_add.format_description(upgrade_strings['description']) ticket_list.append(ticket_to_add.__dict__) # create testing tickets for other tasks for section in CONFIG.sections(): if 'Ticket' in section: strings = config_map(section) ticket_to_add = ticket.Ticket(jira_project, jira_issue_type) ticket_to_add.format_summary(strings['summary'], apprenda_version) ticket_to_add.format_description(strings['description']) ticket_list.append(ticket_to_add.__dict__) print 'Created {0} tickets, now sending them to Jira'.format( len(ticket_list)) # send issues to jira and create tickets and links issues = jira.create_issues(field_list=ticket_list) for item in issues: jira.create_issue_link( type="Task of Story", outwardIssue=item['issue'].key, inwardIssue=parent_issue.key, ) print 'Finished linking issues, exiting.'
class JiraToolsAPI: def __init__(self, jira_server_link, username=None, password=None): """Initalizes the Jira API connector. If a username or password is not provided you will be prompted for it. args: jira_server_link (str): Link to the Jira server to touch API kwargs: username (str): Overwrites jira username prompt password (str): Overwrites jira password prompt return: None """ self.jira_server_link = jira_server_link self.jira_options = {"server": self.jira_server_link} if username == None: username = input("Username: "******"Authenticated successfully with Jira with {self.username}") def create(self, data): """Create a single Jira ticket. args: data (dict): Fields required or needed to create the ticket. return (str): Ticket number / 'False' if fails """ try: jira_ticket = self._JIRA.create_issue(fields=data) logging.info( f"Successfully created Jira issue '{jira_ticket.key}'") return jira_ticket.key except Exception as error: logging.debug( f"Failed to create Jira issue '{jira_ticket.key}'\n\n{error}\n\n" ) return False def link(self, issue_from, issue_to, issue_link_name=None): """Link two issues together. Defaults to 'Relates' unless issue_link_name is specified. args: issue_from (str): Issue that will be linked from. issue_to (str): Issue that will be linked to. kwargs: issue_link_name (str): issue link name that should be applied. return (bool): Will return 'True' if it completed successfully. """ try: self._JIRA.create_issue_link(issue_link_name, issue_from, issue_to) logging.info( f"Successfully created a '{issue_link_name}' link between '{issue_from}' and '{issue_to}'." ) return True except Exception as error: logging.debug( f"Failed to create a link between '{issue_from}' and '{issue_to}'\n\n{error}\n\n" ) return False def label(self, issue, labels): """Apply labels to a given issue. args: issue (str): Issue that labels will be applied to. labels (list): list of labels that should be applied to the issue. Return (bool): Will return 'True' if it completed successfully. """ if type(labels) == list: try: issue_instance = self._JIRA.issue(issue) issue_instance.update( fields={"labels": issue_instance.fields.labels + labels}) logging.info( f"Successfully added labels '{labels}' to '{issue}'") return True except Exception as error: logging.debug( f"Failed to add labels '{labels}' to '{issue}'\n\n{error}\n\n" ) return False else: raise ScriptError('A list must be passed to the labels argument') def comment(self, issue, comment): """Apply a comment to a given issue. args: issue (str): Issue that comment will be applied to. comment (str): comment that should be applied to the issue. return (bool): Will return 'True' if it completed successfully. """ try: self._JIRA.add_comment(issue, comment) logging.info( f"Successfully added comment '{comment}' to '{issue}'") return True except Exception as error: logging.debug( f"Failed to add comment '{comment}' to '{issue}'\n\n{error}\n\n" ) return False def log_work(self, issue, time_spent, comment=None): """Log work to a given issue. args: issue (str): Issue to log work. time_spent (str): Time that should be logged to the issue. kwargs: comment (str): Description of what this time represents. return (bool): Will return 'True' if it completed successfully. """ try: if comment != None and type(comment) == str: self._JIRA.add_worklog(issue, time_spent, comment=comment) else: self._JIRA.add_worklog(issue, time_spent) logging.info(f"Successfully logged time to '{issue}'") return True except Exception as error: logging.info( f"Failed to log work to '{issue}' See debug logs for more.") logging.debug(f"\n{error}\n") return False def add_attachment(self, issue, attachment): """Attach file to Jira issue. args: issue (str): Issue name attachment (str): Location of file that should be attached. Return (bool): Will return 'True' if completed successfully """ assert isinstance(issue, str) assert isinstance(attachment, str) try: self._JIRA.add_attachment(issue=issue, attachment=attachment) logging.info(f'Successfully attached document to "{issue}"') return True except Exception as error: logging.debug( f"Failed to attach document to '{issue}'\n\n{error}\n\n") return False def update_status(self, id, end_status, transfer_statuses=[], timeout_attempts=10): """Change issue to desired status. Due to the workflow features of Jira it might not be possible to transition directly to the wanted status, intermediary statuses might be required and this funcation allows for that using 'transfer_statuses'. args: id (str): Issue id for status update end_status (str): Name of status to update ticket to. kwargs: transfer_statuses (list): Ordered list of intermediary statuses timeout_attempts (num): Number of times before while loop times out. return (bool): Will return 'True' if completed successfully """ while timeout_attempts != 0: transitions = self._JIRA.transitions(id) for transition in transitions: if transition['name'] == end_status: jira_ticket = self._JIRA.transition_issue( id, transition['id']) logging.info( f"Updated status of '{issue}' to '{end_status}'") return True elif transition['name'] in transfer_statuses: jira_ticket = self._JIRA.transition_issue( id, transition['id']) timeout_attempts -= 1 logging.debug( f"Failed to update status of '{id}' to end_status ({end_status})") return False
'issuetype': { 'name': 'Task' }, 'duedate': DueDate5week, 'summary': 'Тестирование {}. Ежедневный контроль производительности по реплеям и бенчмаркам.' .format(Current_Stable), 'priority': Priority_high, 'fixVersions': [{ 'name': '{}'.format(Current_Fix_Versions) }], 'description': 'Перечень тестов оформлен в виде саб-тасков: \n# Тестирование {}. Ежедневный контроль версии на' ' базе реплеев. \n# Тестирование {}. Ежедневный контроль производительности версии на базе' ' BenchmarkLocations.'.format(Current_Stable, Current_Stable), 'components': Component_QA, 'environment': Environment_of_creation_dev_cut_from_, 'assignee': { 'name': 'a_gorchakov' }, } # #Cоздание ишью Task1 = jira.create_issue(fields=TASK1_KONTROL) ## Связь двух ишью в эпик включен таск jira.create_issue_link('is included in', Task1, EpicStory, None) print("created Task1 " + Task1.key)
class JiraAPI: """ Jira client has no documentation, so if you need one, use one for REST API: https://developer.atlassian.com/cloud/jira/platform/rest/v3/ """ def __init__(self, settings: Settings): self._settings = settings self.transition = settings.jira.transition self.release_task = settings.jira.release_task self.release_task_name = self.release_task.name.format( version=settings.version, component=self.release_task.component) self._api = JIRA( {"server": settings.jira.connection.server}, basic_auth=(settings.jira.connection.user, settings.jira.connection.token), ) def _create_version(self, project: Project): proposed_name = "Hotfix" if self._settings.version.minor > 0 else "Release" user_input = input(f"Input new Jira version name: [{proposed_name}]: ") name = user_input if user_input else proposed_name return self._api.create_version(name, project, startDate=_get_formatted_date()) def _select_version(self, project: Project, unreleased_versions) -> Optional[Version]: print("Jira versions:") print("1) Skip") print("2) Create new") print("or select existing one:") unreleased_versions = { idx: version for idx, version in enumerate(unreleased_versions, 3) } for idx, version in unreleased_versions.items(): print(f"{idx}) {version.name}") user_input = 0 valid_choices = list(range(1, len(unreleased_versions) + 3)) while user_input not in valid_choices: try: user_input = int( input( "\nChoose which Jira version use for this release: ")) except Exception: continue if user_input == 1: return None elif user_input == 2: return self._create_version(project) else: return unreleased_versions[user_input] def get_version(self) -> Optional[Version]: print_title(f"Searching for Jira release version") project = self._api.project(self.release_task.project) unreleased_versions = [ v for v in self._api.project_versions(project) if not v.released ] return self._select_version(project, unreleased_versions) def _get_jira_release_unfinished_tasks(self, version: Version): """ To check that all tasks in Jira release is finished select them using jql """ final_statuses = '", "'.join(self.transition.child_final_statuses) types_to_skip = '", "'.join(self.transition.child_task_types_to_skip) return self._api.search_issues( f'project = "{self.release_task.project}"' f' AND fixVersion = "{version.name}"' f' AND fixVersion in unreleasedVersions("{self.release_task.project}")' f' AND status NOT IN ("{final_statuses}")' f' AND type NOT IN ("{types_to_skip}")') def _get_transition(self, issue, transition_name): transitions = [ t for t in self._api.transitions(issue) if t["name"].lower() == transition_name.lower() ] if not transitions: return None return transitions[0] def release_version(self, release_task_key: str): print_title( f"Releasing Jira version of release task {release_task_key}") release_task = self._api.issue(release_task_key) for version in release_task.fields.fixVersions: version: Version print(f'Checking Jira release version: "{version.name}"...') if version.released: print_error("Version is already released") continue unfinished_tasks = self._get_jira_release_unfinished_tasks(version) if unfinished_tasks: tasks_str = ", ".join([i.key for i in unfinished_tasks]) print_error( f'Can\'t release Jira version: "{version.name}", it has unfinished tasks: {tasks_str}' ) continue print("Jira version is safe to release, releasing...", end=" ") version.update(released=True, releaseDate=_get_formatted_date()) print("Ok!") def _add_to_release_version(self, version: Version, release_task_key: str): issue = self._api.issue(release_task_key) issue.add_field_value("fixVersions", {"name": version.name}) def make_links(self, version: Optional[Version], release_task_key, related_keys): version_name = version.name if version else "-" print_title(f"Linking tasks found in release branch" f" to release task ({release_task_key})" f' and to Jira version "{version_name}"') if version: self._add_to_release_version(version, release_task_key) print(f"Linking {len(related_keys)} tasks:") partial_make_links = partial(self._make_links, version, release_task_key) with ThreadPool(5) as pool: pool.map(partial_make_links, related_keys) def _make_links(self, version: Optional[Version], release_task_key: str, child_task_key: str): print(f"* {child_task_key}") self._api.create_issue_link(self.release_task.link_type, release_task_key, child_task_key) if version: self._add_to_release_version(version, child_task_key) def make_release_task(self): print_title("Creating Jira release task") extra_fields = {"components": [{"name": self.release_task.component}]} issue = self._api.create_issue( project=self.release_task.project, summary=self.release_task_name, issuetype={"name": self.release_task.type}, **extra_fields, ) print(f"Created Jira release task: {issue.key}") return issue.key def get_release_task(self): print_title("Searching for Jira release task") query = (f'project = "{self.release_task.project}"' f' AND summary ~ "{self.release_task_name}"' f' AND type = "{self.release_task.type}"') found_issues = self._api.search_issues(query) if not found_issues: print("Did not find existing release task") return self.make_release_task() if len(found_issues) > 1: issues_str = ", ".join([i.key for i in found_issues]) print_error( f"Your release task has not unique name, fix it before using this functionality," f" found issues: {issues_str}") exit(1) release_issue = found_issues[0] print(f"Found Jira release task: {release_issue.key}") return release_issue.key def mark_release_task_done(self, release_task_key): print_title( f'Transition release task "{release_task_key}" from "{self.transition.release_from_status}" to "{self.transition.release_to_status}"' ) release_issue = self._api.issue(release_task_key) print_title( f'Current release task status is "{release_issue.fields.status}"') if (release_issue.fields.status.name.lower() != self.transition.release_from_status.lower()): print_error( f'Release task "{release_task_key}" has inproper status') return transition = self._get_transition(release_issue, self.transition.release_to_status) if not transition: print_error( f'Release task "{release_task_key}" has no transition to "{self.transition.release_to_status}"' ) return self._api.transition_issue(release_issue, transition["id"]) print( f'Release task {release_issue.key} has been transited to status "{transition["name"]}"' ) def mark_children_tasks_done(self, release_task_key): print_title( f'Transition children of "{release_task_key}" from "{self.transition.child_from_status}" to "{self.transition.child_to_status}"' ) query = (f'issue in linkedIssues("{release_task_key}")' f' AND status = "{self.transition.child_from_status}"') found_issues = self._api.search_issues(query) to_status = self.transition.child_to_status.lower() if not found_issues: print("Did not find any task for transition") return for issue in found_issues: transition = self._get_transition(issue, to_status) if not transition: print_error( f'Issue "{issue.key}" does not have transition to status "{self.transition.child_to_status}"' ) continue self._api.transition_issue(issue, transition["id"]) print( f'Task {issue.key} has been transited to status "{transition["name"]}"' )
class jira_client(object): """Simple wrapper around jira api for build baron analyzer needs""" def __init__(self, jira_server, jira_user): self.jira = JIRA(options={ 'server': jira_server, 'verify': False }, basic_auth=(jira_user, jira_client._get_password( jira_server, jira_user)), validate=True) # Since the web server may share this client among threads, use a lock since it unclear if # the JIRA client is thread-safe self._lock = threading.Lock() @staticmethod def _get_password(server, user): global keyring password = None if keyring: try: password = keyring.get_password(server, user) except: print("Failed to get password from keyring") keyring = None if password is not None: print("Using password from system keyring.") else: password = getpass.getpass("Jira Password:"******"Store password in system keyring? (y/N): ").strip() if answer == "y": keyring.set_password(server, user, password) return password def query_duplicates_text(self, fields): search = "project in (bf, server, evg, build) AND (" + " or ".join( ['text~"%s"' % f for f in fields]) + ")" return search def search_issues(self, query, maxResults=50): with self._lock: results = self.jira.search_issues(query, fields=[ "id", "key", "status", "resolution", "summary", "created", "updated", "assignee", "description" ], maxResults=maxResults) print("Found %d results" % len(results)) return results def add_affected_version(self, bf_issue, affected_version_string): """ Adds a new 'affectedVersion' to a BF, or does nothing if the version is already present in the issue's 'affectedVersions'. """ affected_versions = bf_issue.fields.versions if affected_versions is None: affected_versions = [] else: affected_versions = [{"name": v.name} for v in affected_versions] if affected_version_string.lower() == "master": affected_version_string = "3.6" if {"name": affected_version_string} in affected_versions: return affected_versions.append({"name": affected_version_string}) try: bf_issue.update(fields={"versions": affected_versions}) except JIRAError as e: print("Error updating issue's affected versions: " + str(e)) def add_failing_task(self, bf_issue, failing_task_string): """ Adds a new 'failing_task' to a BF, or does nothing if the version is already present in the issue's 'failing_task's. """ failing_tasks = bf_issue.fields.customfield_12950 if failing_tasks is None: failing_tasks = [] if failing_task_string in failing_tasks: return failing_tasks.append(failing_task_string) try: bf_issue.update(fields={"customfield_12950": failing_tasks}) except JIRAError as e: print("Error updating duplicate issue's failing_tasks: " + str(e)) def add_affected_variant(self, bf_issue, variant_string): """ Adds a new 'Buildvariant' to a BF, or does nothing if the version is already present in the issue's 'Buildvariant's, or if 'variant_string' is not a valid Buildvariant specifier. The latter case can happen if it is the name of a variant on an old branch, or if it is a new variant and we haven't updated JIRA yet. """ affected_variants = bf_issue.fields.customfield_11454 if affected_variants is None: affected_variants = [] else: affected_variants = [{"value": v.value} for v in affected_variants] if {"value": variant_string} in affected_variants: return affected_variants.append({"value": variant_string}) try: bf_issue.update(fields={"customfield_11454": affected_variants}) except JIRAError as e: print("Error updating duplicate issue's Buildvariants: " + str(e)) def add_github_backtrace_context(self, issue_string, backtrace): """ Given a backtrace represented as follows, appends some nicely formatted previews of the code involved in the backtrace and adds them to the issue's description. A backtrace is a list of frames, specified as follows: { "github_url": "https://github.com/mongodb/mongo/blob/deadbeef/jstests/core/test.js#L42", "first_line_number": 37, "line_number": 42, "frame_number": 0, "file_path": "jstests/core/test.js", "file_name": "test.js", "lines": ["line 37", "line 38", ..., "line 47"] } """ new_description_lines = [""] for frame in backtrace: frame_title = "Frame {frame_number}: [{path}:{line_number}|{url}]".format( frame_number=frame["frame_number"], path=frame["file_path"], line_number=frame["line_number"], url=frame["github_url"]) # raw text is 0-based, code lines are 1-based. first_line = frame["first_line_number"] + 1 code_block_header = ( "{code:js|title=%s|linenumbers=true|firstline=%d|highlight=%d}" % (frame["file_name"], first_line, frame["line_number"])) new_description_lines.append("") new_description_lines.append(frame_title) new_description_lines.append(code_block_header) new_description_lines.extend(frame["lines"]) new_description_lines.append("{code}") self.append_to_issue_description(issue_string, new_description_lines) def add_fault_comment(self, issue_string, fault): """ Adds a {noformat} block to the jira ticket's description summarizing the fault that we have extracted from the logs. """ self.append_to_issue_description(issue_string, [ "", "Extracted {fault_type}: ".format(fault_type=fault.category), "{noformat}", fault.context, "{noformat}" ]) def append_to_issue_description(self, issue_string, new_lines): """ Adds the given text to the bottom of the given issue's description. If the ticket has already been tagged with the 'bot-analyzed' tag, no update occurs. """ jira_issue = self.get_bfg_issue(issue_string) try: if "bot-analyzed" not in jira_issue.fields.labels: print("Updating issue description: \n" + "\n".join(new_lines)) jira_issue.update(description=jira_issue.fields.description + "\n".join(new_lines)) else: print( "Skipping issue update, since issue is already tagged with 'bot-analyzed'" ) except JIRAError as e: print("Error updating JIRA: " + str(e)) # jira.resolutions() # <JIRA Resolution: name='Fixed', id='1'> # <JIRA Resolution: name="Won't Fix", id='2'> # <JIRA Resolution: name='Duplicate', id='3'> # <JIRA Resolution: name='Incomplete', id='4'> # <JIRA Resolution: name='Cannot Reproduce', id='5'> # <JIRA Resolution: name='Works as Designed', id='6'> # <JIRA Resolution: name='Gone away', id='7'> # <JIRA Resolution: name='Community Answered', id='8'> # <JIRA Resolution: name='Done', id='9'> # Issue link Types # <JIRA IssueLinkType: name='Backports', id='10420'> # <JIRA IssueLinkType: name='Depends', id='10011'> # <JIRA IssueLinkType: name='Documented', id='10320'> # <JIRA IssueLinkType: name='Duplicate', id='10010'> # <JIRA IssueLinkType: name='Gantt Dependency', id='10020'> # <JIRA IssueLinkType: name='Gantt End to End', id='10423'> # <JIRA IssueLinkType: name='Gantt End to Start', id='10421'> # <JIRA IssueLinkType: name='Gantt Start to End', id='10424'> # <JIRA IssueLinkType: name='Gantt Start to Start', id='10422'> # <JIRA IssueLinkType: name='Related', id='10012'> # <JIRA IssueLinkType: name='Tested', id='10220'> def close_as_duplicate(self, issue, duplicate_issue): with self._lock: src_issue = self.jira.issue(issue) dest_issue = self.jira.issue(duplicate_issue) # Add duplicate link self.jira.create_issue_link(type='Duplicate', inwardIssue=issue, outwardIssue=duplicate_issue) # Update affectsVersions, Buildvariants, etc. title_parsing_regex = re.compile( "(Timed Out|Failures?):" " (?P<suite_name>.*?)" " on" " (?P<variant_prefix>[^\(\[]+)" "(?P<variant_suffix>" " (?:" "\(Clang 3\.7/libc\+\+\)|" "\(No Journal\)|" "\(inMemory\)|" "\(ephemeralForTest\)|" "\(Unoptimized\))(?: DEBUG)?)?" " (?:\(" "(?P<test_names>(.*?(\.js|CheckReplDBHash)(, )?)*)" "\))? ?\[MongoDB \(" "(?P<version>.*?)" "\) @ [0-9A-Za-z]+\]") parsed_title = title_parsing_regex.match(src_issue.fields.summary) if parsed_title is not None: # Update the failing variants. variant = parsed_title.group("variant_prefix").rstrip() if parsed_title.group("variant_suffix") is not None: variant += parsed_title.group("variant_suffix").rstrip() self.add_affected_variant(dest_issue, variant) # Update the failing tasks. self.add_failing_task(dest_issue, parsed_title.group("suite_name")) # Update the affected versions. self.add_affected_version(dest_issue, parsed_title.group("version")) # Close - id 2 # Duplicate issue is 3 self.jira.transition_issue(src_issue, '2', resolution={'id': '3'}) def close_as_goneaway(self, issue): with self._lock: src_issue = self.jira.issue(issue) # Close - id 2 # Gone away is 7 self.jira.transition_issue(src_issue, '2', comment="Transient machine issue.", resolution={'id': '7'}) def get_bfg_issue(self, issue_number): if not issue_number.startswith("BFG-") and not issue_number.startswith( "BF-"): issue_number = "BFG-" + issue_number with self._lock: src_issue = self.jira.issue(issue_number) return src_issue
class JiraOperations(object): """ Base class for interaction with JIRA """ def __init__(self, config): # do not print excess warnings urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # JIRA configuration from config.json/DDB self.config = config # JIRA url self.server = self.config.jira.server # JIRA established session self.session = None if self.config.jira.enabled: self.login_oauth() else: logging.debug("JIRA integration is disabled") @property def current_user(self): """ :return: JIRA user name, used for connection establishing """ return self.session.current_user() def login_oauth(self): """ Establish JIRA connection using oauth :return: boolean, if connection was successful. """ if not self.config.jira.credentials: logging.error("Failed to login jira (empty credentials)") return False try: self.session = JIRA(options={ 'server': self.server, 'verify': False }, oauth=self.config.jira.credentials["oauth"]) except JIRAError: logging.exception( f"Failed to create oauth session to {self.server}") return False logging.debug( f'JIRA session to {self.server} created successfully (oauth)') return True def login_basic(self): """ Establish JIRA connection using basic authentication :return: boolean, if connection was successful. """ if not self.config.jira.credentials: logging.error("Failed to login jira (empty credentials)") return False username = self.config.jira.credentials["basic"]["username"] password = self.config.jira.credentials["basic"]["password"] options = {'server': self.server, 'verify': False} try: self.session = JIRA(options, basic_auth=(username, password)) except Exception: logging.exception( f"Failed to create basic session to {self.server}") return False logging.debug( f'JIRA session to {self.server} created successfully (basic)') return True def ticket_url(self, ticket_id): """ :return: URL to `ticket_id` """ return f"{self.server}/browse/{ticket_id}" def ticket_assignee(self, ticket_id): """ :param ticket_id: JIRA ticket :return: name of current assignee for ticket """ ticket = self.session.issue(ticket_id) return ticket.fields.assignee.name def find_valid_assignee(self, project, assignees): """ Check what record from given list of possible assignees can be used as assignee for given project. :param project: name of Jira project to perform check against :param assignees: list of possible assignees :return: """ for assignee in assignees: if assignee is None: continue try: users = self.session.search_assignable_users_for_projects( assignee, project) except Exception: continue # check only exact matches if len(users) == 1: return users[0].name return None def create_ticket(self, issue_data): """ Create a JIRA ticket :param issue_data: a dict containing field names and the values to use """ resp = self.session.create_issue(fields=issue_data) logging.debug(f"Created jira ticket {self.ticket_url(resp.key)}") return resp.key def create_issue_link(self, inward_issue, outward_issue): """ Linking JIRA tickets with 'relates to' link :return: boolean, if linking was successful """ if not (inward_issue or outward_issue): return False try: # JIRA comes with default types of links: # 1) relates to / relates to, # 2) duplicates / is duplicated by, # 3) blocks / is blocked by # 4) clones / is cloned by link_type = "relates to" self.session.create_issue_link(type=link_type, inwardIssue=inward_issue, outwardIssue=outward_issue) except Exception: logging.exception( f"Failed to create issue link {inward_issue} -> {outward_issue}" ) return False logging.debug(f"Created issue link {inward_issue} -> {outward_issue}") return True def assign_user(self, ticket_id, assinee_name): """ Assign `ticket_id` to `assinee_name`. :return: boolean, if assigning was successful """ if not (ticket_id or assinee_name): return False try: issue = self.session.issue(ticket_id) issue.update(assignee={'name': assinee_name}) except Exception: logging.exception( f"Failed to assign {ticket_id} to {assinee_name}") return False logging.debug(f"Assigned {ticket_id} to {assinee_name}") return True def add_label(self, ticket_id, label): """ add label to `ticket_id`. :return: boolean, if label update was successful """ if not (ticket_id and label): return False try: issue = self.session.issue(ticket_id) issue.fields.labels.append(label) issue.update(fields={"labels": issue.fields.labels}) except Exception: logging.exception(f"Failed to add {label} to {ticket_id}") return False logging.debug(f"Added label {label} to {ticket_id}") return True def update_ticket(self, ticket_id, updated_issue_data): """ Update JIRA ticket fields as in self.create_ticket(), but for existing ticket :param ticket_id: ticket Id to update :param updated_issue_data: a dict containing field names and the values to use :return: boolean, if updating was successful """ try: issue = self.session.issue(ticket_id) issue.update(updated_issue_data) except Exception: logging.exception(f"Failed to update {ticket_id}") return False logging.debug(f"Updated {ticket_id}") return True def add_comment(self, ticket_id, comment): """ Add comment to JIRA ticket :param ticket_id: ticket Id to add comment to :param comment: comment text :return: boolean, if operation was successful """ if ticket_id and comment: try: self.session.add_comment(ticket_id, comment) except Exception: logging.exception(f"Failed to add comment to {ticket_id}") return False return True def add_watcher(self, ticket_id, user): """ Adding jira ticket watcher. :param ticket_id: jira ticket id :param user: watcher user id :return: nothing """ self.session.add_watcher(ticket_id, user) def close_issue(self, ticket_id): """ Transition of ticket to `Closed` state. It checks if issue can be transitioned to `Closed` state. :param ticket_id: ticket Id to close :return: nothing """ if not ticket_id: return issue = self.session.issue(ticket_id) if issue.fields.status.name == "Closed": logging.debug(f"{ticket_id} is already closed") return for transition in self.session.transitions(issue): if transition['name'] == 'Close Issue': self.session.transition_issue(ticket_id, transition['id']) logging.debug(f"Closed {ticket_id}") break else: logging.error(f"{self.ticket_url(ticket_id)} can't be closed") return def resolve_issue(self, ticket_id): """ Transition of ticket to `Resolved` state. It checks if issue can be transitioned to `Resolved` state. :param ticket_id: ticket Id to resolve :return: nothing """ issue = self.session.issue(ticket_id) if issue.fields.status.name == "Resolved": logging.debug(f"{ticket_id} is already resolved") return for transition in self.session.transitions(issue): if transition['name'] == 'Resolve Issue': self.session.transition_issue(ticket_id, transition['id']) logging.debug(f"Resolved {ticket_id}") break else: logging.error(f"{self.ticket_url(ticket_id)} can't be resolved") return def reopen_issue(self, ticket_id): """ Transition of ticket to `Reopen Issue` state. It checks if issue can be transitioned to `Reopen Issue` state. :param ticket_id: ticket Id to reopen :return: nothing """ issue = self.session.issue(ticket_id) if issue.fields.status.name in ["Open", "Reopened"]: logging.debug(f"{ticket_id} is already opened") return for transition in self.session.transitions(issue): if transition['name'] == 'Reopen Issue': self.session.transition_issue(ticket_id, transition['id']) logging.debug(f"Reopened {ticket_id}") break else: logging.error(f"{self.ticket_url(ticket_id)} can't be reopened") return def add_attachment(self, ticket_id, filename, text): """ Add text as attachment with filename to JIRA ticket :param ticket_id: ticket Id to add attachment to :param filename: label for attachment :param text: attachment text :return: attachment object """ attachment = io.StringIO(text) filename = filename.replace(':', '-') return self.session.add_attachment(issue=ticket_id, attachment=attachment, filename=filename) @staticmethod def build_tags_table(tags): """ Build JIRA table from AWS tags dictionary :param tags: dict with tags :return: str with JIRA table """ if not tags: return "" desc = f"*Tags*:\n" desc += f"||Key||Value||\n" for key, value in tags.items(): desc += f"|{key}|{empty_converter(value)}|\n" return desc
class jira_creator: def __init__(self, jira_href, login, password, file, author=None): #инициализация класса, получение переменных self.jira_href = jira_href self.login = login self.password = password self.file = file self.jira = None self.effect = 'Выполнение' self.target = 'Учет' self.author = author def __duedate_confirm(self, str_date): try: str_date = re.search('\d{4}-\d{2}-\d{2}', str(str_date))[0] return str_date except: return None def __permiss_list( self, permiss_str ): #вспомогательная функция для подготовки списка массивов прав доступа dicts_list = [] try: for i in permiss_str.split(','): dicts_list.append({'name': i}) return dicts_list except: dicts_list.append({'name': ''}) return dicts_list def __assert_DF(self, DF, manager_ia, manager_project): #проверка данных в датафрейме if type(manager_ia) != str and manager_ia.find(',') != -1: raise Exception( 'Exception! In "manager_ia" column finded ",". Maybe many users in cell' ) if type(manager_project) != str and manager_project.find(',') != -1: raise Exception( 'Exception! In "manager_project" column finded ",". Maybe many users in cell' ) if True in DF.assignee.str.contains(',').values: raise Exception( 'Exception! In "assignee" column finded ",". Maybe many users in cell' ) if False in (DF.customfield_11630 > DF.customfield_11610).values: raise Exception('Exception! date of end < startdate') if False in (DF.duedate > DF.customfield_11610).values: raise Exception('Exception! duedate < startdate') if True in (DF.summary.apply(len) > 150).values: raise Exception( 'Exception! More than 150 characters in the "summary" column') def __prepare_df( self, file ): #функция подготовки полученной таблицы к нужному для jira виду DF = pd.read_excel(file) list_decepen = [] DF = DF.rename({'Проект':'project', 'Тип_задачи':'issuetype', 'Заголовок':'summary', 'Описание':'description',\ 'Дата_Начала':'customfield_11610', 'Планируемая_дата_выполнения':'customfield_11630', 'Срок_исполнения':'duedate',\ 'Метка':'labels', 'Менеджер_ВА':'customfield_21400', 'Менеджер_проекта':'customfield_11622',\ 'Доступ_к_задаче':'customfield_10909', 'Ответственный':'assignee'}, axis=1) DF['customfield_11627'] = self.effect DF['customfield_11651'] = self.target DF.summary = DF.summary.apply( lambda x: x.replace('"', '').replace('\\', '/')) DF.project = DF.project.apply(lambda x: {'key': x}) DF.issuetype = DF.issuetype.apply(lambda x: {'name': x}) DF.labels = DF.labels.apply(lambda x: x.split(',')) DF.customfield_10909 = DF.customfield_10909.apply(self.__permiss_list) DF.customfield_11610 = DF.customfield_11610.apply( lambda x: re.search('\d{4}-\d{2}-\d{2}', str(x))[0]) DF.customfield_11630 = DF.customfield_11630.apply( lambda x: re.search('\d{4}-\d{2}-\d{2}', str(x))[0]) DF.duedate = DF.duedate.apply(self.__duedate_confirm) list_df = DF.issuetype.tolist() for j, i in enumerate(list_df): if i['name'] == 'Проект': list_decepen.append(j) elif i['name'] == 'Веха': k = j if list_df[j - 1] == 'Задача': k = j list_decepen.append(k) elif i['name'] == 'Задача': list_decepen.append(k) DF['hierarchy'] = list_decepen list_unique = list(set(list_decepen)) manager_ia = DF.customfield_21400[0] manager_project = DF.customfield_11622[0] DF = DF.drop(['customfield_21400', 'customfield_11622'], axis=1) self.__assert_DF(DF, manager_ia, manager_project) return DF, list_unique, manager_ia, manager_project def __assign_update( self, key, assignee, manager_ia, manager_project): #функция для назначения задачи на ответственного issue = self.jira.issue(key) issue.update( fields={ 'assignee': { 'name': assignee }, 'customfield_21400': { 'name': manager_ia }, 'customfield_11622': { 'name': manager_project } }) if self.author: try: issue.update(fields={'reporter': {'name': self.author}}) except: logging.warning('cannot change author in {}'.format(key)) #def __project_mark_cell_to (self, key, project_id, mark_id): #маркировка проектом и вехой # issue = self.jira.issue(key) # issue.update(fields={'customfield_22700':[str(project_id)]}) # issue.update(fields={'customfield_22701':[str(mark_id)]}) def __create_project_issue(self, dict_project_issue): #создание проекта project_issue = self.jira.create_issue(fields=dict_project_issue) id_project = project_issue.id key_project = project_issue.key logging.info('{} is create'.format(key_project)) return key_project, id_project def __create_mark_issue(self, dict_mark_issue): #создание вехи mark_issue = self.jira.create_issue(fields=dict_mark_issue) id_mark_issue = mark_issue.id key_mark_issue = mark_issue.key logging.info('{} is create'.format(key_mark_issue)) return key_mark_issue, id_mark_issue def __create_task_issue(self, dict_task_issue): #создание задачи task_issue = self.jira.create_issue(fields=dict_task_issue) key_task_issue = task_issue.key id_task_issue = task_issue.id logging.info('{} is create'.format(key_task_issue)) return key_task_issue, id_task_issue def __jira_auth(self): #авторизация options = {"server": self.jira_href} self.jira = JIRA(options, basic_auth=(self.login, self.password)) return self.jira def __search_double_issue( self, dict_issue, assignee): #функция проверки существования задачи logging.info('{} :check for existence '.format(dict_issue['summary'])) search_by_description = self.jira.search_issues( 'project=IAIT AND summary ~"{}"'.format(dict_issue['summary'])) if search_by_description: if search_by_description.fields.summary == dict_issue[ 'summary'] and search_by_description.fields.assignee.name == assignee: logging.info('{} :already exist'.format(dict_issue['summary'])) return search_by_description[0].key, search_by_description[ 0].id, False else: if dict_issue['issuetype']['name'] == 'Проект': key, id = self.__create_project_issue(dict_issue) return key, id, True elif dict_issue['issuetype']['name'] == 'Веха': key, id = self.__create_mark_issue(dict_issue) return key, id, True elif dict_issue['issuetype']['name'] == 'Задача': key, id = self.__create_task_issue(dict_issue) return key, id, True else: raise Exception(dict_issue['issuetype']['name'], ' is unsupport name of issue') def IAIT_create_project( self): #основное тело для вызова функций и преобразования данных if self.jira == None: self.jira = self.__jira_auth() DF, list_unique, manager_ia, manager_project = self.__prepare_df( self.file) DF_project = DF[DF.hierarchy == list_unique[0]].drop( ['hierarchy', 'duedate'], axis=1) dict_project = DF_project.to_dict('r')[0] assignee_issue = dict_project.pop('assignee') project_key, project_id, project_not_exist = self.__search_double_issue( dict_project, assignee_issue) if project_not_exist: self.__assign_update(project_key, assignee_issue, manager_ia, manager_project) for i in list_unique[1:]: DF_marks = DF[DF.hierarchy == i] DF_marks["customfield_22700"] = DF_marks.summary.apply( lambda x: [project_id]) dict_marks = DF_marks.iloc[:1, :].drop([ 'customfield_11627', 'customfield_11651', 'hierarchy', 'duedate' ], axis=1).to_dict('r')[0] assignee_issue = dict_marks.pop('assignee') mark_key, mark_id, mark_not_exist = self.__search_double_issue( dict_marks, assignee_issue) if mark_not_exist: self.__assign_update(mark_key, assignee_issue, manager_ia, manager_project) self.jira.create_issue_link(type="Иерархия задач", inwardIssue=project_key, outwardIssue=mark_key) #self.__project_mark_cell_to(mark_key, project_id, mark_id) logging.info('{} is create link to {}'.format( mark_key, project_key)) DF_tasks = DF_marks.iloc[1:, :].drop( ['customfield_11627', 'customfield_11651', 'hierarchy'], axis=1) DF_tasks["customfield_22701"] = DF_tasks.summary.apply( lambda x: [mark_id]) DF_tasks["customfield_22700"] = DF_tasks.summary.apply( lambda x: [project_id]) dict_tasks = DF_tasks.to_dict('r') for task in dict_tasks: assignee_issue = task.pop('assignee') task_key, task_id, task_not_exist = self.__search_double_issue( task, assignee_issue) if task_not_exist: self.__assign_update(task_key, assignee_issue, manager_ia, manager_project) logging.info('{} is create link to {}'.format( task_key, mark_key))
class CoachesHelpDesk: def __init__(self, domain='jira.fiware.org'): self.base_url = 'https://{}'.format(domain) self.user = JIRA_USER self.password = JIRA_PASSWORD options = {'server': self.base_url, 'verify': False} self.jira = JIRA(options=options, basic_auth=(self.user, self.password)) self.n_assignment = 0 self.n_clones = 0 self.n_renamed = 0 def assign_request(self): query = 'project = HELC AND issuetype = extRequest AND component = EMPTY' requests = sorted(self.jira.search_issues(query, maxResults=False), key=lambda item: item.key) for request in requests: summary = request.fields.summary if re.search(r'\[SPAM\]', summary): request.update(fields={'components': [{'name': 'SPAM'}]}) continue match = re.search(r'\[[^\]]+?\]', summary) if match: accelerator = match.group(0)[1:-1] if accelerator in accelerators_dict: components = {'name': accelerators_dict[accelerator]} # request.update(fields={'components':[components]}, assignee={'name': '-1'}) request.update(fields={'components': [components]}) if not request.fields.assignee: self.jira.assign_issue(request, '-1') self.n_assignment += 1 logging.info('updated request {}, accelerator= {}'.format( request, accelerator)) def clone_to_main(self): self._clone_tech() self._clone_lab() def _clone_tech(self): query = 'project = HELC AND issuetype = extRequest AND component = _TECH_ AND not assignee = EMPTY' requests = sorted(self.jira.search_issues(query, maxResults=False), key=lambda item: item.key) for request in requests: fields = { 'project': { 'key': 'HELP' }, 'components': [{ 'name': 'FIWARE-TECH-HELP' }], 'summary': request.fields.summary, 'description': request.fields.description, 'issuetype': { 'name': request.fields.issuetype.name }, 'priority': { 'name': request.fields.priority.name }, 'labels': request.fields.labels, 'assignee': { 'name': None }, 'reporter': { 'name': request.fields.reporter.name } } new_issue = self.jira.create_issue(fields=fields) self.jira.create_issue_link('relates to', new_issue, request) self.jira.add_watcher(new_issue, request.fields.assignee.name) self.jira.remove_watcher(new_issue, self.user) components = [{ 'name': comp.name } for comp in request.fields.components if comp.name != '_TECH_'] request.update(fields={'components': components}) # self.jira.add_watcher(new_issue, request.fields.assignee.name) logging.info('CREATED TECH ISSUE: {} from {}'.format( new_issue, request)) self.n_clones += 1 def _clone_lab(self): query = 'project = HELC AND issuetype = extRequest AND component = _LAB_ AND not assignee = EMPTY' requests = sorted(self.jira.search_issues(query, maxResults=False), key=lambda item: item.key) for request in requests: fields = { 'project': { 'key': 'HELP' }, 'components': [{ 'name': 'FIWARE-LAB-HELP' }], 'summary': request.fields.summary, 'description': request.fields.description, 'issuetype': { 'name': request.fields.issuetype.name }, 'priority': { 'name': request.fields.priority.name }, 'labels': request.fields.labels, 'assignee': { 'name': None }, 'reporter': { 'name': request.fields.reporter.name } } new_issue = self.jira.create_issue(fields=fields) self.jira.create_issue_link('relates to', new_issue, request) self.jira.add_watcher(new_issue, request.fields.assignee.name) self.jira.remove_watcher(new_issue, self.user) components = [{ 'name': comp.name } for comp in request.fields.components if comp.name != '_LAB_'] request.update(fields={'components': components}) logging.info('CREATED LAB ISSUE: {} from {}'.format( new_issue, request)) self.n_clones += 1 def naming(self): query = 'project = HELC AND issuetype = extRequest AND status = Closed and updated >= -1d' requests = sorted(self.jira.search_issues(query, maxResults=False), key=lambda item: item.key) for request in requests: component = request.fields.components[0].name summary = request.fields.summary if re.match(r'FIWARE.Request.Coach.{}'.format(component), summary): continue summary = re.sub(r'\[[^\]]+?\]', '', summary) summary = 'FIWARE.Request.Coach.{}.{}'.format( component, summary.strip()) request.update(summary=summary) logging.info('{} {} {} {}'.format(request, request.fields.status, component, summary)) self.n_renamed += 1