def getEm(): options = { 'server': 'https://jira.server.com' } jira = JIRA( options=options, basic_auth=('user','pass') ) tickets = jira.search_issues( 'Participants = currentUser() AND updated > startOfDay() ORDER BY updated DESC' ) for ticket in tickets: comments = jira.comments( ticket.key ) for comment in comments: if comment.author.name == 'username': t = comment.created t = unicodedata.normalize( 'NFKD', t ).encode( 'UTF-8', 'ignore' ) t = t.split("T") t = t[0] now = time.strftime("%Y-%m-%d") if t == now: minute_len = len( comment.body.split() ) if minute_len <= 40: minutes = "3m" else: minutes = str( int( round( math.ceil( minute_len/40 )*3 ) ) ) + "m" jira.add_worklog( issue=str(ticket.key), timeSpent=minutes )
class JiraService(IssueService): # A map of jira priorities to taskwarrior priorities priorities = {"Trivial": "L", "Minor": "L", "Major": "M", "Critical": "H", "Blocker": "H"} def __init__(self, *args, **kw): super(JiraService, self).__init__(*args, **kw) self.username = self.config.get(self.target, "jira.username") self.url = self.config.get(self.target, "jira.base_uri") self.query = "assignee=" + self.username + " AND status != closed and status != resolved" self.jira = JIRA( options={"server": self.config.get(self.target, "jira.base_uri")}, basic_auth=(self.username, self.config.get(self.target, "jira.password")), ) @classmethod def validate_config(cls, config, target): for option in ["jira.username", "jira.password", "jira.base_uri"]: if not config.has_option(target, option): die("[%s] has no '%s'" % (target, option)) IssueService.validate_config(config, target) def get_owner(self, issue): return True def annotations(self, issue): annotations = [] comments = self.jira.comments(issue) if comments is []: pass else: for comment in comments: created = date.fromtimestamp(time.mktime(time.strptime(comment.created[0:10], "%Y-%m-%d"))) annotations.append(self.format_annotation(created, comment.author.name, comment.body)) return dict(annotations) def issues(self): cases = self.jira.search_issues(self.query, maxResults=-1) log.debug(" Found {0} total.", len(cases)) return [ dict( description=self.description( title=case.fields.summary, url=self.url + "/browse/" + case.key, number=case.key.rsplit("-", 1)[1], cls="issue", ), project=case.key.rsplit("-", 1)[0], priority=self.priorities.get(get_priority(case.fields.priority), self.default_priority), **self.annotations(case.key) ) for case in cases ]
def getEm( streamlined ): options = { 'server': 'https://jira.server.com' } jira = JIRA( options=options, basic_auth=('username','password') ) tickets = jira.search_issues( '("Dev Lead"=currentUser() OR assignee = currentUser()) AND ( "Last Resolution Date" > startOfWeek() OR status != Closed)' ) for ticket in tickets: comments = jira.comments( ticket.key ) for comment in comments: if comment.author.name == 'automation': answ = 'y' # for streamlining the process if streamlined == False: answ = raw_input( str(comment.body)[:80] + ' - ' + '\033[93m' + 'DELETE y/n? ' + '\033[0m' ) if answ == 'y': print 'Deleting comment...' comment.delete() print 'Deleted.' else: print 'Ok - I will ask later on.'
class JiraService(IssueService): # A map of jira priorities to taskwarrior priorities priorities = { 'Trivial': 'L', 'Minor': 'L', 'Major': 'M', 'Critical': 'H', 'Blocker': 'H', } def __init__(self, *args, **kw): super(JiraService, self).__init__(*args, **kw) self.username = self.config.get(self.target, 'jira.username') self.url = self.config.get(self.target, 'jira.base_uri') password = self.config.get(self.target, 'jira.password') if not password or password.startswith("@oracle:"): service = "jira://%s@%s" % (self.username, self.url) password = get_service_password(service, self.username, oracle=password, interactive=self.config.interactive) default_query = 'assignee=' + self.username + \ ' AND status != closed and status != resolved' self.query = self.config.get(self.target, 'jira.query', default_query) self.project_prefix = self.config.get( self.target, 'jira.project_prefix', '') self.jira = JIRA( options={ 'server': self.config.get(self.target, 'jira.base_uri'), 'rest_api_version': 'latest', }, basic_auth=(self.username, password) ) @classmethod def validate_config(cls, config, target): for option in ['jira.username', 'jira.password', 'jira.base_uri']: if not config.has_option(target, option): die("[%s] has no '%s'" % (target, option)) IssueService.validate_config(config, target) def get_owner(self, issue): return True def annotations(self, issue): annotations = [] comments = self.jira.comments(issue) if comments is []: pass else: for comment in comments: created = date.fromtimestamp(time.mktime(time.strptime( comment.created[0:10], '%Y-%m-%d'))) annotations.append(self.format_annotation( created, comment.author.name, comment.body)) return dict(annotations) def __convert_for_jira4(self, issue): print(issue.key) class IssueWrapper: pass #print(self.jira.issue(issue.key).fields.summary.value) #print(self.jira.issue(issue.key).fields.summary) new_issue = self.jira.issue(issue.key) result = IssueWrapper() fields = IssueWrapper() fields.__dict__ = { 'summary': new_issue.fields.summary.value, 'priority': new_issue.fields.priority.name, } result.__dict__ = { 'key': issue.key, 'fields': fields, } return result def __issue(self, case, jira_version): result = dict( description=self.description( title=case.fields.summary, url=self.url + '/browse/' + case.key, number=case.key.rsplit('-', 1)[1], cls="issue"), project=self.project_prefix + case.key.rsplit('-', 1)[0], priority=self.priorities.get( get_priority(case.fields.priority), self.default_priority, ) ) if jira_version != 4: result.update(self.annotations(case.key)) return result
class JiraService(IssueService): ISSUE_CLASS = JiraIssue CONFIG_PREFIX = 'jira' def __init__(self, *args, **kw): super(JiraService, self).__init__(*args, **kw) self.username = self.config_get('username') self.url = self.config_get('base_uri') password = self.config_get('password') if not password or password.startswith("@oracle:"): password = get_service_password( self.get_keyring_service(self.config, self.target), self.username, oracle=password, interactive=self.config.interactive) default_query = 'assignee=' + self.username + \ ' AND resolution is null' self.query = self.config_get_default('query', default_query) self.jira = JIRA(options={ 'server': self.config_get('base_uri'), 'rest_api_version': 'latest', 'verify': self.config_get_default('verify_ssl', default=None, to_type=asbool), }, basic_auth=(self.username, password)) self.import_labels_as_tags = self.config_get_default( 'import_labels_as_tags', default=False, to_type=asbool) self.label_template = self.config_get_default('label_template', default='{{label}}', to_type=six.text_type) @classmethod def get_keyring_service(cls, config, section): username = config.get(section, cls._get_key('username')) base_uri = config.get(section, cls._get_key('base_uri')) return "jira://%s@%s" % (username, base_uri) def get_service_metadata(self): return { 'url': self.url, 'import_labels_as_tags': self.import_labels_as_tags, 'label_template': self.label_template, } @classmethod def validate_config(cls, config, target): for option in ('jira.username', 'jira.password', 'jira.base_uri'): if not config.has_option(target, option): die("[%s] has no '%s'" % (target, option)) IssueService.validate_config(config, target) def annotations(self, issue, issue_obj): comments = self.jira.comments(issue.key) if not comments: return [] else: return self.build_annotations( ((comment.author.name, comment.body) for comment in comments), issue_obj.get_processed_url(issue_obj.get_url())) def issues(self): cases = self.jira.search_issues(self.query, maxResults=-1) jira_version = 5 if self.config.has_option(self.target, 'jira.version'): jira_version = self.config.getint(self.target, 'jira.version') for case in cases: issue = self.get_issue_for_record(case.raw) extra = { 'jira_version': jira_version, } if jira_version > 4: extra.update({'annotations': self.annotations(case, issue)}) issue.update_extra(extra) yield issue
class JiraService(IssueService): ISSUE_CLASS = JiraIssue CONFIG_PREFIX = "jira" def __init__(self, *args, **kw): super(JiraService, self).__init__(*args, **kw) self.username = self.config_get("username") self.url = self.config_get("base_uri") password = self.config_get("password") if not password or password.startswith("@oracle:"): password = get_service_password( self.get_keyring_service(self.config, self.target), self.username, oracle=password, interactive=self.config.interactive, ) default_query = "assignee=" + self.username + " AND resolution is null" self.query = self.config_get_default("query", default_query) self.jira = JIRA( options={ "server": self.config_get("base_uri"), "rest_api_version": "latest", "verify": self.config_get_default("verify_ssl", default=None, to_type=asbool), }, basic_auth=(self.username, password), ) self.import_labels_as_tags = self.config_get_default("import_labels_as_tags", default=False, to_type=asbool) self.label_template = self.config_get_default("label_template", default="{{label}}", to_type=six.text_type) @classmethod def get_keyring_service(cls, config, section): username = config.get(section, cls._get_key("username")) base_uri = config.get(section, cls._get_key("base_uri")) return "jira://%s@%s" % (username, base_uri) def get_service_metadata(self): return { "url": self.url, "import_labels_as_tags": self.import_labels_as_tags, "label_template": self.label_template, } @classmethod def validate_config(cls, config, target): for option in ("jira.username", "jira.password", "jira.base_uri"): if not config.has_option(target, option): die("[%s] has no '%s'" % (target, option)) IssueService.validate_config(config, target) def annotations(self, issue, issue_obj): comments = self.jira.comments(issue.key) if not comments: return [] else: return self.build_annotations( ((comment.author.name, comment.body) for comment in comments), issue_obj.get_processed_url(issue_obj.get_url()), ) def issues(self): cases = self.jira.search_issues(self.query, maxResults=-1) jira_version = 5 if self.config.has_option(self.target, "jira.version"): jira_version = self.config.getint(self.target, "jira.version") for case in cases: issue = self.get_issue_for_record(case.raw) extra = {"jira_version": jira_version} if jira_version > 4: extra.update({"annotations": self.annotations(case, issue)}) issue.update_extra(extra) yield issue
class JiraService(IssueService): ISSUE_CLASS = JiraIssue CONFIG_PREFIX = 'jira' def __init__(self, *args, **kw): super(JiraService, self).__init__(*args, **kw) self.username = self.config_get('username') self.url = self.config_get('base_uri') password = self.config_get('password') if not password or password.startswith("@oracle:"): password = get_service_password( self.get_keyring_service(self.config, self.target), self.username, oracle=password, interactive=self.config.interactive ) default_query = 'assignee=' + self.username + \ ' AND status != closed and status != resolved' self.query = self.config_get_default('query', default_query) self.jira = JIRA( options={ 'server': self.config_get('base_uri'), 'rest_api_version': 'latest', 'verify': self.config_get_default('verify_ssl', default=None, to_type=asbool), }, basic_auth=(self.username, password) ) self.import_labels_as_tags = self.config_get_default( 'import_labels_as_tags', default=False, to_type=asbool ) self.label_template = self.config_get_default( 'label_template', default='{{label}}', to_type=six.text_type ) @classmethod def get_keyring_service(cls, config, section): username = config.get(section, cls._get_key('username')) base_uri = config.get(section, cls._get_key('base_uri')) return "jira://%s@%s" % (username, base_uri) def get_service_metadata(self): return { 'url': self.url, 'import_labels_as_tags': self.import_labels_as_tags, 'label_template': self.label_template, } @classmethod def validate_config(cls, config, target): for option in ('jira.username', 'jira.password', 'jira.base_uri'): if not config.has_option(target, option): die("[%s] has no '%s'" % (target, option)) IssueService.validate_config(config, target) def annotations(self, issue, issue_obj): comments = self.jira.comments(issue.key) if not comments: return [] else: return self.build_annotations( (( comment.author.name, comment.body ) for comment in comments), issue_obj.get_processed_url(issue_obj.get_url()) ) def issues(self): cases = self.jira.search_issues(self.query, maxResults=-1) jira_version = 5 if self.config.has_option(self.target, 'jira.version'): jira_version = self.config.getint(self.target, 'jira.version') for case in cases: issue = self.get_issue_for_record(case.raw) extra = { 'jira_version': jira_version, } if jira_version > 4: extra.update({ 'annotations': self.annotations(case, issue) }) issue.update_extra(extra) yield issue
msg.attach( MIMEText(emailtext, 'html') ) smtp = smtplib.SMTP(emailsmtp) smtp.sendmail(emailfrom, emailto, msg.as_string()) smtp.close() print "email sent to " + emailto # Summary of my last 10 assigned issues emailbody = "<b>Found these JIRAs:</b><BR>" emailbodynocommentsthisweek = "<BR><hr><b>Found these JIRAs without comments this week:</b><BR>" emailbodynocomments = "<BR><hr><b>Found these JIRAs without comments:</b><BR>" for issue in jira.search_issues('assignee = currentUser() and project=ACD order by updated desc', maxResults=10): print issue print "Issue summary: " + issue.fields.summary numcomments = 0 numcommentsthisweek = 0 for comment in jira.comments(issue): numcomments += 1 author = str(comment.author) author = author.strip(' ,\t\n\r') if author == myname: print "Comment by: " + myname commentdate = parse(str(comment.updated)) print "comment updated on: " + str(commentdate) if commentdate >= lastweek: if numcomments == 1: emailbody += "<BR>" + str(issue) + ": " + issue.fields.summary + "<BR>" numcommentsthisweek += 1 print "Ok, comment after: " + str(lastweek) print comment.body emailbody += unicode(comment.body).encode('ascii', 'ignore') + "<BR>" else:
') to (' + jira_issue.key + ')') jira_issue.fields.labels.append(new_issue_label) jira_issue.update( fields={"labels": jira_issue.fields.labels}) # Once the JIRA ticket is created, should the security level be modified? if config.getboolean('jira', 'modify_security_level'): original_security_level = config.get( 'jira', 'original_security_level') original_security_level_mentioned = False if original_security_level != "None": if original_security_level in jira_issue.fields.description: original_security_level_mentioned = True else: # Obtain all current comments on the JIRA ticket. jira_comments = jira.comments(jira_issue) for existing_comment in jira_comments: if original_security_level in existing_comment.body: original_security_level_mentioned = True break if original_security_level_mentioned: logger.debug( 'Original security level for JIRA ticket (' + jira_issue.key + ') mentioned') syslog.syslog( syslog.LOG_DEBUG, 'Original security level for JIRA ticket (' + jira_issue.key + ') mentioned') # If checks passed, then modify the security level.
class JeerahClient(object): def __init__(self, conf): self.conf = conf self.credentials = CredentialsConfig(self.conf.site.credentials) self.c = None self.issues_created = [] self.abort_on_error = True self.results_format = 'list' self.versions_cache = {} def connect(self): if self.c is None: self.c = JIRA(options={'server': self.conf.site.url}, basic_auth=(self.credentials.jira.username, self.credentials.jira.password)) logger.debug('created jira connection') else: logger.debug('jira connection exists') logger.debug('configured user: '******'actual user: '******'{0}' to version cache".format(ver.name)) if project not in self.versions_cache: self.versions_cache[project] = {} self.versions_cache[project][ver.name] = ver.id def create_issue(self, title, text, assignee, project, reporter=None, tags=None, version=None, uid=None): issue = { 'project': { 'key': project }, 'issuetype': { 'name': 'Task' }, 'summary': title, 'description': text, 'assignee': { 'name': assignee } } if reporter is not None: issue['reporter'] = {'name': reporter} if tags is not None: issue['labels'] = [tags] if version is not None: if project not in self.versions_cache: logger.debug( "updating version cache to include {0} versions".format( project)) self.update_version_cache(project) if version not in self.versions_cache[project]: logger.error("version {0} doesn't exist in {1} project".format( version, project)) else: issue['fixVersions'] = [{ 'id': self.versions_cache[project][version] }] logger.debug('adding version to issue: {0}'.format( issue['fixVersions'])) new_issue = self.c.create_issue(fields=issue) logger.debug('created new issue {0}'.format(new_issue.key)) self.issues_created.append({ 'key': new_issue.key, 'uid': uid, 'title': title }) def query(self, query_string): logger.info('running query for: {0}'.format(query_string)) try: query_results = self.c.search_issues(jql_str=query_string, maxResults=200) except Exception as e: logger.warning(query_string) logger.error(e) if self.abort_on_error is True: raise SystemExit(e) if self.results_format == 'dict': return {issue.key: issue for issue in query_results} elif self.results_format == 'list': return [issue for issue in query_results] def versions(self, project, released=False, archived=False): return [ v for v in self.c.project_versions(project) if v.released is released and v.archived is archived ] def release_version(self, version): if not isinstance(version, Version): logger.error('{0} is not a jira version.'.format(version)) else: logger.info('releasing version {0}'.format(version.name)) version.update(released=True) def archive_version(self, version): if not isinstance(version, Version): logger.error('{0} is not a jira version.'.format(version)) else: logger.info('archiving version {0}'.format(version.name)) version.update(archived=True) def create_version(self, project, name, description='', release=None): if release is None: release = str(datetime.date.today() + datetime.timedelta(days=14)) elif release is False: release = None elif isinstance(release, datetime.date): release = str(release) self.c.create_version(name=name, project=project, description=description, releaseDate=release) logger.debug('created version {0} in project {0}'.format( name, project))
class Housekeeping(): """ This class is the container for all automated Jira functions performed by the Housekeeping agent. """ def __init__(self): # class variables self.ac_label = u'auto-close-24-hours' # open JIRA API Connection self.jira = JIRA(options=secrets.options, basic_auth=secrets.housekeeping_auth) # commands to run self.content_acquisition_auto_qc() self.requeue_free_indexing() self.auto_assign() self.remind_reporter_to_close() self.close_resolved() self.clear_auto_close_label() self.resolved_issue_audit() self.handle_audited_tickets() def content_acquisition_auto_qc(self): """ Takes INDEXREP issues that have been Merged for 30+ minutes and transitions them to Quality Control. It then adds a comment that tags the reporter to inform them that the issue is ready for review. """ # get CA tickets merged 30+ minute ago issues = self.get_issues("auto_qc") for issue in issues: reporter = issue.fields.reporter.key message = '[~%s], this issue is ready for QC.' % reporter """ 771 is the transition ID spedific to this step for this project. Anything more generic will need to parse the transitions list. """ tran_id = self.get_transition_id(issue,"qc") self.jira.transition_issue(issue.key,tran_id) self.jira.add_comment(issue.key, message) def handle_audited_tickets(self): """ Handles audit tickets that are failed. Closed tickets are ignored. Failed tickets trigger the creation of a new ticket in the same project as the original ticket. Inputs: None Returns: None """ issues = self.jira.search_issues( # get all the ADT issues 'project=ADT and status="Failed Audit"') # For each failed issue, generate a new work ticket then close this one for issue in issues: #BUID adt_buid=issue.fields.customfield_10502 #WCID adt_wcid=issue.fields.customfield_10501 #oldBUID adt_old_buid=issue.fields.customfield_13100 #oldWCID adt_old_wcid=issue.fields.customfield_13101 #Indexing Type adt_indexing_type=issue.fields.customfield_10500 #comments adt_comments = [] for comment in self.jira.comments(issue): node = { 'body':self.jira.comment(issue,comment).body, 'author': self.jira.comment(issue,comment).author.key } adt_comments.append(node) link_list = [issue.key,] # first linked ticket should be this audit ticket for link in issue.fields.issuelinks: # grab the rest of links try: link_list.append(link.outwardIssue.key) except AttributeError: pass # capture original ticket and project. If it can't be found, default to INDEXREP as the project try: original_ticket = issue.fields.summary.split("[")[1].split("]")[0] original_project = original_ticket.split("-")[0] except IndexError: original_ticket = "" original_project="INDEXREP" # build the new summary by parsing the audit summary indexrep_summary = issue.fields.summary #build the summary indexrep_summary = indexrep_summary.replace("compliance audit - ","") indexrep_summary = indexrep_summary.split("[")[0] indexrep_summary = ' %s - Failed Audit' % (indexrep_summary) # Build the issue description message = 'This issue failed audit. Please review %s and make any \ necessary corrections.' % original_ticket # Construct the watcher list and de-dupe it watcher_list = [issue.fields.assignee.key,] for w in self.jira.watchers(issue).watchers: watcher_list.append(w.key) watcher_list = set(watcher_list) # get the reporter (reporter is preserved from audit to issue) reporter = issue.fields.reporter.key # Generate the new issue, then close the audit ticket. new_issue = self.make_new_issue(original_project,"",reporter, indexrep_summary,message, watcher_list,link_list,adt_buid, adt_wcid,adt_old_buid,adt_old_wcid, adt_indexing_type,adt_comments) close_me = self.close_issue(issue.key) def resolved_issue_audit(self): """ TAKES issues that have been resolved from specified projectsfor a set interval and creates a new ticket in AUDIT, closes the INDEXREP ticket, and then assigns it to the audit user specified in the self.qa_auditor role. Inputs: None Returns: Error message or Nothing """ # get all the issues from projects in the audit list issues = self.get_issues("audit_list") #build a list of all users in the MS & MD groups member_svc = self.get_group_members("member-services") member_dev = self.get_group_members("membership-development") member_aud = self.get_group_members("issue audits") member_all = [] for user in member_svc: member_all.append(user) #only need the user names, not the meta data for user in member_dev: member_all.append(user) for user in member_aud: member_all.append(user) member_all = set(member_all) #de-dupe # cycle through them and create a new ADT ticket for each for issue in issues: # capture issue fields ind_buid=issue.fields.customfield_10502 #BUID ind_wcid=issue.fields.customfield_10501 #WCID ind_old_buid=issue.fields.customfield_13100 #oldBUID ind_old_wcid=issue.fields.customfield_13101 #oldWCID ind_indexing_type=issue.fields.customfield_10500 #Indexing Type reporter = issue.fields.reporter.key #Reporter link_list = [issue.key,] for link in issue.fields.issuelinks: # grab the rest of links try: link_list.append(link.outwardIssue.key) except AttributeError: pass # build the new ticket summary based on the issue being audited # [ISSUE=123] is used to preserve the original issue key. Replace any brackets with () # to prevent read errors later. adt_summary = issue.fields.summary.replace("[","(").replace("]",")") adt_summary = 'compliance audit - %s [%s]' % (adt_summary,issue.key) # check reporter to see if special consideration is needed # if reporter is not MS or MD, or it's a new member, assign to audit lead. new_member_setup = self.check_for_text(issue, settings.member_setup_strs) assigned_audit_tasks_query = self.get_issues("assigned_audits",True) if reporter not in member_all or new_member_setup: qa_auditor = self.user_with_fewest_issues('issue audits lead', assigned_audit_tasks_query, [reporter]) else: # get the users who can be assigned audit tickets, then select the # one with fewest assigned tickets qa_auditor = self.user_with_fewest_issues('issue audits', assigned_audit_tasks_query, [reporter]) # build the description message = '[~%s], issue %s is ready to audit.' % (qa_auditor, issue.key) #build the watcher list, including original reporter and assignee of the audited ticket watcher_list = [] for w in self.jira.watchers(issue).watchers: watcher_list.append(w.key) try: original_assignee = issue.fields.assignee.key except AttributeError: original_assignee="" # make the audit ticket new_issue = self.make_new_issue("ADT",qa_auditor,reporter, adt_summary,message,watcher_list,link_list,ind_buid, ind_wcid,ind_old_buid,ind_old_wcid,ind_indexing_type) # close the INDEXREP ticket close_me = self.close_issue(issue.key) # add comment to indexrep ticket link_back_comment = "This issue has been closed. The audit ticket is %s" % new_issue self.jira.add_comment(issue.key, link_back_comment) def requeue_free_indexing(self): """ Takes a list of old FCA tickets, and clears the assignee fields in order to allow it to be reassigned. Inputs: None Returns: Nothing """ # get issues that are stale and need reassigned issues = self.get_issues("stale_free") # itirate issues and set assignee to empty. This will allow # auto assignment to set the assignee. for issue in issues: #check for wait in label wait_label = self.label_contains(issue,"wait") # if no wait label, clear the assignee so it can be re-autoassigned if (not wait_label): issue.update(assignee={'name':""}) def make_new_issue(self,project,issue_assignee,issue_reporter,summary, description="",watchers=[],links=[], buid="",wcid="",old_buid="",old_wcid="", indexing_type="",comments=[], issuetype="Task"): """ Creates a new issue with the given parameters. Inputs: *REQUIRED* :project: the jira project key in which to create the issue :issue_assignee: user name who the issue will be assigned to :issue_reporter: user name of the issue report :summary: string value of the issue summary field *OPTIONAL* :description: Issue description. Defaults to empty string :watchers: list of user names to add as issue watchers :link: list of issue keys to link to the issue as "Related To" :issuetype: the type of issue to create. Defaults to type. :buid: business unit - custom field 10502 :wcid: wrapping company id - custom field 10501 :old_buid: old business unit - custom field 13100 :old_wcid: old wrapping company id - custom field 13101 :indexing_type: the indexing type - custom field 10500 :comments: list dictionaries of comments and authors to auto add. Returns: Jira Issue Object """ issue_dict = { 'project':{'key':project}, 'summary': summary, 'issuetype': {'name':issuetype}, 'description':description, } new_issue = self.jira.create_issue(fields=issue_dict) # assign the audit tick to auditor new_issue.update(assignee={'name':issue_assignee}) new_issue.update(reporter={'name':issue_reporter}) # add watchers to audit ticket (reporter, assignee, wacthers from indexrep ticket) for watcher in watchers: try: self.jira.add_watcher(new_issue,watcher) except: pass # link the audit ticket back to indexrep ticket for link in links: self.jira.create_issue_link('Relates',new_issue,link) # add custom field values if set if buid: new_issue.update(fields={'customfield_10502':buid}) if wcid: new_issue.update(fields={'customfield_10501':wcid}) if indexing_type: new_issue.update(fields={'customfield_10500':{'value':indexing_type.value}}) if old_buid: new_issue.update(fields={'customfield_13100':old_buid}) if old_wcid: new_issue.update(fields={'customfield_13101':old_wcid}) # add comments quoted_comments = "" for comment in comments: quoted_comments = "%s[~%s] Said:{quote}%s{quote}\\\ \\\ " % (quoted_comments,comment['author'],comment['body']) if quoted_comments: quoted_comments = "Comments from the parent issue:\\\ %s" % quoted_comments self.jira.add_comment(new_issue,quoted_comments) return new_issue # method to transistion audit ticket def get_group_members(self, group_name): """ Returns the members of a group as a list """ group = self.jira.groups(query=group_name)[0] members = self.jira.group_members(group) return members def auto_assign(self,project="INDEXREP"): """ Looks up new INDEXREP issues with an empty assignee and non-agent reporter and assigns them to the user in the content-acquisition user group with the fewest assigned contect-acquistion tickets. """ # get member INDEXREP issues that need to auto assigned mem_issues = self.get_issues("member_auto_assign") # get free indexing requests free_issues = self.get_issues("free_auto_assign") # get unassigned member engagement issues mer_issues = self.get_issues("mer_auto_assign") # get unassigned sales engineering issues se_issues = self.get_issues("se_auto_assign") # get non-resolved assigned Member issues member_assigned_issues_query = self.get_issues("member_assigned_issues",True) # get non-resolved assigned Free Indexing issues free_assigned_issues_query = self.get_issues("free_assigned_issues",True) # get non-resolved member enagement issues mer_assigned_issues_query = self.get_issues("mer_assigned_issues",True) # get non-resolved sales engineering issues se_assigned_issues_query = self.get_issues("se_assigned_issues",True) def _assign(issue,username): """ Private method for assigning an issue. Inputs: issue: issue to assign username: person to assign the issue """ reporter = issue.fields.reporter.key self.jira.assign_issue(issue=issue,assignee=username) message = ("[~%s], this issue has been automically assigned " "to [~%s].") % (reporter,username) self.jira.add_comment(issue.key, message) auto_assign_dicts = [ { "issue_list": mem_issues, "assigned_list": member_assigned_issues_query, "assignee_group": "content-acquisition", }, { "issue_list": free_issues, "assigned_list": free_assigned_issues_query, "assignee_group": "content-acquisition-free", }, { "issue_list": mer_issues, "assigned_list": mer_assigned_issues_query, "assignee_group": "mer-assignees", "watch_list":"mer-auto-watch", }, { "issue_list": se_issues, "assigned_list": se_assigned_issues_query, "assignee_group": "se-assignees", }] for auto_assign_dict in auto_assign_dicts: for issue in auto_assign_dict["issue_list"]: username = self.user_with_fewest_issues(auto_assign_dict["assignee_group"], auto_assign_dict["assigned_list"]) if (auto_assign_dict["issue_list"]==mem_issues or auto_assign_dict["issue_list"]==free_issues): # check if the indexing type is already set. If so, do nothing. if issue.fields.customfield_10500 == None: # default to member indexing for issues in mem_issues if auto_assign_dict["issue_list"]==mem_issues: issue.update({"customfield_10500":{"id":"10103"}}) elif auto_assign_dict["issue_list"]==free_issues: free_index_mem = self.get_group_members("free-index-default") # set the indexing type to free if the reporter is in the list # of users who default to free if issue.fields.reporter.key in free_index_mem: issue.update({"customfield_10500":{"id":"10100"}}) #free else: #default is member otherwise issue.update({"customfield_10500":{"id":"10103"}}) # if the dict object has a watch list item, add default watchers if "watch_list" in auto_assign_dict: watchers = self.get_group_members(auto_assign_dict["watch_list"]) self.toggle_watchers("add",issue,watchers) _assign(issue,username) def remind_reporter_to_close(self): """ Comments on all non-closed resolved issues that are 13 days without a change. Notifies the reporter it will be closed in 24 hours and adds a label to the issue that is used as a lookup key by the close method. """ issues = self.get_issues("remind_close_issues") for issue in issues: reporter = issue.fields.reporter.key message = ( "[~%s], this issue has been resolved for 13 days. It will be " "closed automatically in 24 hours.") % reporter watch_list = self.toggle_watchers("remove",issue) self.jira.add_comment(issue.key,message) issue.fields.labels.append(self.ac_label) issue.update(fields={"labels": issue.fields.labels}) self.toggle_watchers("add",issue, watch_list) def close_resolved(self): """ Looks up all issues labeled for auto-closing that have not been updated in 24 hours and closes them. Ignores INDEXREP so as to not interfere with the auditing process. """ issues = self.get_issues("auto_close_issues") for issue in issues: reporter = issue.fields.reporter.key message = ( "[~%s], this issue has closed automatically.") % reporter close_me = self.close_issue(issue) self.jira.add_comment(issue.key,message) def get_transition_id(self,issue,key): """ Finds the transition id for an issue given a specific search string. Inputs: key: search string issue: jira issue Returns: transition id or False """ trans = self.jira.transitions(issue) tran_id = False for tran in trans: tran_name = tran['name'].lower() if key in tran_name: tran_id = tran['id'] return tran_id def close_issue(self, issue): """ Closes the issue passed to it with a resolution of fixed. Inputs: Issue: the issue object to close Returns: True|False """ trans = self.jira.transitions(issue) success_flag = False tran_id = self.get_transition_id(issue,"close") if not tran_id: tran_id = self.get_transition_id(issue,"complete") if tran_id: try: self.jira.transition_issue(issue,tran_id, {'resolution':{'id':'1'}}) #some close transitions don't have a resolution screen except: #open ended, but the JIRAError exception is broken. self.jira.transition_issue(issue,tran_id) success_flag = True return success_flag def clear_auto_close_label(self): """ Clears the auto-close label from issues that have been re-opened since the auto-close reminder was posted. """ issues = self.jira.search_issues( 'status in ("Quality Control", Reopened, Merged, open) \ AND labels in (auto-close-24-hours)') for issue in issues: label_list = issue.fields.labels watch_list = self.toggle_watchers("remove",issue) label_list.remove(self.ac_label) issue.update(fields={"labels": label_list}) self.toggle_watchers("add",issue, watch_list) def toggle_watchers(self,action,issue,watch_list=[]): """ Internal method that either adds or removes the watchers of an issue. If it removes them,it returns a list of users that were removed. If it adds, it returns an updated list of watchers. Inputs: :action: String "add"|"remove". The action to take :issue: Issue whose watchers list is being modified :watch_list: list of users. Optional for remove. Required for add. Returns: :issue_watcher: List of users who are or were watching the issue. """ if action=="remove": issue_watchers = self.jira.watchers(issue).watchers for issue_watcher in issue_watchers: # watch list can be inconsensent when returned by the jira api # same issue in the add loop try: self.jira.remove_watcher(issue,issue_watcher.name) except AttributeError: self.jira.add_watcher(issue,old_watcher) else: for old_watcher in watch_list: try: self.jira.add_watcher(issue,old_watcher.name) except AttributeError: self.jira.add_watcher(issue,old_watcher) issue_watchers = self.jira.watchers(issue).watchers return issue_watchers def label_contains(self,issue,search_string): """ Internal method that searches the labels of an issue for a given string value. It allows filtering that is roughly "labels ~ 'string'", which is not supported by JQL. Inputs: :issue: Jira issue object that is being checked :search_string: the string value being checked for Returns: True|False True if search_string exists in any label. """ return any(search_string in label for label in issue.fields.labels) def check_for_text(self,issue,text_list): """ Internal method that searches the summary and description of an issue for a given list of strings. Match is non-case sensative, and converts everything to lower case before checking for a match. Inputs: :issue: Jira issue object that is being checked :text_list: strings to checked for Returns: True|False True if any of the values in text_list exist. """ string_exists = False if issue.fields.summary: summary = issue.fields.summary.lower() else: summary = "" if issue.fields.description: description = issue.fields.description.lower() else: description = "" for text in text_list: text = text.lower() if text in summary or text in description: string_exists = True return string_exists def user_with_fewest_issues(self,group,query,blacklist=[]): """ Given a query, return the username of the use with the fewest assigned issues in the result set. Inputs: group: the group of users for which to count issues. query: the issues to lookup. Should be a JQL string. blacklist: (optional) list of inelligible users """ members = self.get_group_members(group) issues = self.jira.search_issues(query,maxResults=1000) member_count = {} for member in members: member_count[member]=0 # perform the count anew for each ticket for issue in issues: if issue.fields.assignee: assignee = issue.fields.assignee.key else: assignee = None if assignee in members and not self.label_contains(issue,"wait"): member_count[assignee] = member_count[assignee]+1 #sort the list so that the user with the lowest count is first member_count_sorted = sorted(member_count.items(), key=operator.itemgetter(1)) # prevent assignment to a user in the blacklist, so long as there are # at least 2 available users while (str(member_count_sorted[0][0]) in blacklist and len(member_count_sorted)>1): del member_count_sorted[0] # return the username of the user return str(member_count_sorted[0][0]) def get_issues(self,filter_key,return_jql=False): """ Returns issues found using a jira filter. Inputs: :filter_key: the dict key for the filter in settings :return_jql: flag to return JQL instead on issues Returns: :issues: Jira Issues object (default) or JQL string """ filter_id = secrets.jira_filters[filter_key] jql_query = self.jira.filter(filter_id).jql if return_jql: # some functionality needs the JQL instead of an issue list # notably the method self.user_with_fewest_issues return jql_query else: issues = self.jira.search_issues(jql_query) return issues
class Housekeeping(): """ This class is the container for all automated Jira functions performed by the Housekeeping agent. """ def __init__(self): # class variables self.ac_label = u'auto-close-24-hours' self.audit_delay = '-72h' self.audit_projects = "INDEXREP" #comma delimited project keys # open JIRA API Connection self.jira = JIRA(options=secrets.options, basic_auth=secrets.housekeeping_auth) # commands to run self.content_acquisition_auto_qc() self.auto_assign() self.remind_reporter_to_close() self.close_resolved() self.clear_auto_close_label() self.resolved_issue_audit() self.handle_audited_tickets() def content_acquisition_auto_qc(self): """ Takes INDEXREP issues that have been Merged for 30+ minutes and transitions them to Quality Control. It then adds a comment that tags the reporter to inform them that the issue is ready for review. """ issues = self.jira.search_issues( 'project=INDEXREP and status=Merged and updated<="-30m"') for issue in issues: reporter = issue.fields.reporter.key message = '[~%s], this issue is ready for QC.' % reporter """ 771 is the transition ID spedific to this step for this project. Anything more generic will need to parse the transitions list. """ self.jira.transition_issue(issue.key,'771') self.jira.add_comment(issue.key, message) def handle_audited_tickets(self): """ Handles audit tickets that are failed. Closed tickets are ignored. Failed tickets trigger the creation of a new ticket in the same project as the original ticket. Inputs: None Returns: None """ issues = self.jira.search_issues( # get all the ADT issues 'project=ADT and status="Failed Audit"') # For each failed issue, generate a new work ticket then close this one for issue in issues: #BUID adt_buid=issue.fields.customfield_10502 #WCID adt_wcid=issue.fields.customfield_10501 #Indexing Type adt_indexing_type=issue.fields.customfield_10500 #comments adt_comments = [] for comment in self.jira.comments(issue): node = { 'body':self.jira.comment(issue,comment).body, 'author': self.jira.comment(issue,comment).author.key } adt_comments.append(node) link_list = [issue.key,] # first linked ticket should be this audit ticket for link in issue.fields.issuelinks: # grab the rest of links try: link_list.append(link.outwardIssue.key) except AttributeError: pass # capture orignal tick and project original_ticket = issue.fields.summary.split("[")[1].split("]")[0] original_project = original_ticket.split("-")[0] # build the new summary by parsing the audit summary indexrep_summary = issue.fields.summary #build the summary indexrep_summary = indexrep_summary.replace("compliance audit - ","") indexrep_summary = indexrep_summary.split("[")[0] indexrep_summary = ' %s - Failed Audit' % (indexrep_summary) # Build the issue description message = 'This issue failed audit. Please review %s and make any \ necessary corrections.' % original_ticket # Construct the watcher list and de-dupe it watcher_list = [issue.fields.assignee.key,] for w in self.jira.watchers(issue).watchers: watcher_list.append(w.key) watcher_list = set(watcher_list) # get the reporter (reporter is preserved from audit to issue) reporter = issue.fields.reporter.key # Generate the new issue, then close the audit ticket. new_issue = self.make_new_issue(original_project,"EMPTY",reporter, indexrep_summary,message, watcher_list,link_list,adt_buid, adt_wcid,adt_indexing_type,adt_comments) close_me = self.close_issue(issue.key) def resolved_issue_audit(self,delay="",projects=""): """ TAKES issues that have been resolved from specified projectsfor a set interval and creates a new ticket in AUDIT, closes the INDEXREP ticket, and then assigns it to the audit user specified in the self.qa_auditor role. Inputs: :delay: how long an issue should be resoved before being picked up by this script. Defaults to class level variable :projects: which projects are subject to auditing. Defaults to class level variable Returns: Error message or Nothing """ delay = self.audit_delay if not delay else delay projects = self.audit_projects if not projects else projects # get all the issues from projects in the audit list issue_query = 'project in (%s) and status=Resolved and resolutiondate \ <="%s"' % (projects,delay) issues = self.jira.search_issues(issue_query) # get the users who can be assigned audit tickets. This should be just one person qa_members = self.get_group_members("issue audits") if len(qa_members)==1: qa_auditor=qa_members.keys()[0] else: # for now, throw an error. Later, assign to user with fewer ADT tickets # this will also mean turning the code in auto_assign into a method (DRY) return "Error: There is more than one possible auditor" # cycle through them and create a new ADT ticket for each for issue in issues: #BUID ind_buid=issue.fields.customfield_10502 #WCID ind_wcid=issue.fields.customfield_10501 #Indexing Type ind_indexing_type=issue.fields.customfield_10500 link_list = [issue.key,] for link in issue.fields.issuelinks: # grab the rest of links try: link_list.append(link.outwardIssue.key) except AttributeError: pass # build the new ticket summary based on the issue being audited # [ISSUE=123] is used to preserve the original issue key. Replace any brackets with () # to prevent read errors later. adt_summary = issue.fields.summary.replace("[","(").replace("]",")") adt_summary = 'compliance audit - %s [%s]' % (adt_summary,issue.key) # build the description message = '[~%s], issue %s is ready to audit.' % (qa_auditor, issue.key) #build the watcher list, including original reporter and assignee of the audited ticket watcher_list = [] for w in self.jira.watchers(issue).watchers: watcher_list.append(w.key) reporter = issue.fields.reporter.key try: original_assignee = issue.fields.assignee.key except AttributeError: original_assignee="EMPTY" # make the audit ticket new_issue = self.make_new_issue("ADT",qa_auditor,reporter, adt_summary,message,watcher_list,link_list,ind_buid, ind_wcid,ind_indexing_type) # close the INDEXREP ticket close_me = self.close_issue(issue.key) # add comment to indexrep ticket link_back_comment = "This issue has been closed. The audit ticket is %s" % new_issue self.jira.add_comment(issue.key, link_back_comment) def make_new_issue(self,project,issue_assignee,issue_reporter,summary, description="",watchers=[],links=[],buid="",wcid="", indexing_type="",comments=[],issuetype="Task"): """ Creates a new issue with the given parameters. Inputs: *REQUIRED* :project: the jira project key in which to create the issue :issue_assignee: user name who the issue will be assigned to :issue_reporter: user name of the issue report :summary: string value of the issue summary field *OPTIONAL* :description: Issue description. Defaults to empty string :watchers: list of user names to add as issue watchers :link: list of issue keys to link to the issue as "Related To" :issuetype: the type of issue to create. Defaults to type. :buid: business unit - custom field 10502 :wcid: wrapping company id - custom field 10501 :indexing_type: the indexing type - custom field 10500 :comments: list dictionaries of comments and authors to auto add. Returns: Jira Issue Object """ issue_dict = { 'project':{'key':project}, 'summary': summary, 'issuetype': {'name':issuetype}, 'description':description, } new_issue = self.jira.create_issue(fields=issue_dict) # assign the audit tick to auditor new_issue.update(assignee={'name':issue_assignee}) new_issue.update(reporter={'name':issue_reporter}) # add watchers to audit ticket (reporter, assignee, wacthers from indexrep ticket) for watcher in watchers: self.jira.add_watcher(new_issue,watcher) # link the audit ticket back to indexrep ticket for link in links: self.jira.create_issue_link('Relates',new_issue,link) # add custom field values if set if buid: new_issue.update(fields={'customfield_10502':buid}) if wcid: new_issue.update(fields={'customfield_10501':wcid}) if indexing_type: new_issue.update(fields={'customfield_10500':{'value':indexing_type.value}}) # add comments quoted_comments = "" for comment in comments: quoted_comments = "%s[~%s] Said:{quote}%s{quote}\\\ \\\ " % (quoted_comments,comment['author'],comment['body']) if quoted_comments: quoted_comments = "Comments from the parent issue:\\\ %s" % quoted_comments self.jira.add_comment(new_issue,quoted_comments) return new_issue # method to transistion audit ticket def get_group_members(self, group_name): """ Returns the members of a """ group = self.jira.groups( query=group_name )['groups'][0]['name'] members = self.jira.group_members(group) return members def auto_assign(self): """ Looks up new INDEXREP issues with an empty assignee and non-agent reporter and assigns them to the user in the content-acquisition user group with the fewest assigned contect-acquistion tickets. """ # filter 20702 returns issues that need to auto assigned jql_query = self.jira.filter("20702").jql issues = self.jira.search_issues(jql_query) #filter 21200 returns non-resolved assigned issues assigned_issues_query = self.jira.filter("21200").jql # cycle through each issue and assign it to the user in # content acquisition with the fewest assigned tickets for issue in issues: username = self.user_with_fewest_issues('content-acquisition', assigned_issues_query) reporter = issue.fields.reporter.key watch_list = self.toggle_watchers("remove",issue) self.jira.assign_issue(issue=issue,assignee=username) message = ("[~%s], this issue has been automically assigned " "to [~%s].") % (reporter,username) self.jira.add_comment(issue.key, message) self.toggle_watchers("add",issue,watch_list) def remind_reporter_to_close(self): """ Comments on all non-closed resolved issues that are 13 days without a change. Notifies the reporter it will be closed in 24 hours and adds a label to the issue that is used as a lookup key by the close method. """ issues = self.jira.search_issues( 'resolution != EMPTY AND \ status not in (closed, "Quality Control", Reopened, Merged, open) \ AND updated <= -13d and project not in (INDEXREP)') for issue in issues: reporter = issue.fields.reporter.key message = ( "[~%s], this issue has been resolved for 13 days. It will be " "closed automatically in 24 hours.") % reporter watch_list = self.toggle_watchers("remove",issue) self.jira.add_comment(issue.key,message) issue.fields.labels.append(self.ac_label) issue.update(fields={"labels": issue.fields.labels}) self.toggle_watchers("add",issue, watch_list) def close_resolved(self): """ Looks up all issues labeled for auto-closing that have not been updated in 24 hours and closes them. Ignores INDEXREP so as to not interfere with the auditing process. """ issues = self.jira.search_issues( 'resolution != EMPTY AND \ status not in (closed, "Quality Control", Reopened, Merged, open, \ passed,staged) AND project not in (INDEXREP) \ AND updated <= -24h \ AND labels in (auto-close-24-hours)') for issue in issues: reporter = issue.fields.reporter.key message = ( "[~%s], this issue has closed automatically.") % reporter close_me = self.close_issue(issue) self.jira.add_comment(issue.key,message) def close_issue(self, issue): """ Closes the issue passed to it with a resolution of fixed. Inputs: Issue: the issue object to close Returns: True|False """ trans = self.jira.transitions(issue) success_flag = False for tran in trans: tran_name = tran['name'].lower() if 'close' in tran_name or 'complete' in tran_name: try: self.jira.transition_issue(issue,tran['id'],{'resolution':{'id':'1'}}) #some close transitions don't have a resolution screen except: #open ended, but the JIRAError exception is broken. self.jira.transition_issue(issue,tran['id']) success_flag = True return success_flag def clear_auto_close_label(self): """ Clears the auto-close label from issues that have been re-opened since the auto-close reminder was posted. """ issues = self.jira.search_issues( 'status in ("Quality Control", Reopened, Merged, open) \ AND labels in (auto-close-24-hours)') for issue in issues: label_list = issue.fields.labels watch_list = self.toggle_watchers("remove",issue) label_list.remove(self.ac_label) issue.update(fields={"labels": label_list}) self.toggle_watchers("add",issue, watch_list) def toggle_watchers(self,action,issue,watch_list=[]): """ Internal method that either adds or removes the watchers of an issue. If it removes them,it returns a list of users that were removed. If it adds, it returns an updated list of watchers. Inputs: :action: String "add"|"remove". The action to take :issue: Issue whose watchers list is being modified :watch_list: list of users. Optional for remove. Required for add. Returns: :issue_watcher: List of users who are or were watching the issue. """ if action=="remove": issue_watchers = self.jira.watchers(issue).watchers for issue_watcher in issue_watchers: self.jira.remove_watcher(issue,issue_watcher.name) else: for old_watcher in watch_list: self.jira.add_watcher(issue,old_watcher.name) issue_watchers = self.jira.watchers(issue).watchers return issue_watchers def label_contains(self,issue,search_string): """ Internal method that searches the labels of an issue for a given string value. It allows filtering that is roughly "labels ~ 'string'", which is not supported by JQL. Inputs: :issue: Jira issue object that is being checked :search_string: the string value being checked for Returns: True|False True if search_string exists in any label. """ return any(search_string in label for label in issue.fields.labels) def user_with_fewest_issues(self,group,query): """ Given a query, return the username of the use with the fewest assigned issues in the result set. Inputs: group: the group of users for which to count issues. query: the issues to lookup. Should be a JQL string. """ members = self.get_group_members(group) issues = self.jira.search_issues(query) member_count = {} for member in members: member_count[member]=0 # perform the count anew for each ticket for issue in issues: if issue.fields.assignee: assignee = issue.fields.assignee.key else: assignee = None if assignee in members and not self.label_contains(issue,"wait"): member_count[assignee] = member_count[assignee]+1 #sort the list so that the user with the lowest count is first member_count_sorted = sorted(member_count.items(), key=operator.itemgetter(1)) # return the username of the user return str(member_count_sorted[0][0])
class Housekeeping(): """ This class is the container for all automated Jira functions performed by the Housekeeping agent. """ def __init__(self): # class variables self.ac_label = u'auto-close-24-hours' self.audit_delay = '-72h' self.audit_projects = "INDEXREP" #comma delimited project keys # open JIRA API Connection self.jira = JIRA(options=secrets.options, basic_auth=secrets.housekeeping_auth) # commands to run self.content_acquisition_auto_qc() self.auto_assign() self.remind_reporter_to_close() self.close_resolved() self.clear_auto_close_label() self.resolved_issue_audit() self.handle_audited_tickets() def content_acquisition_auto_qc(self): """ Takes INDEXREP issues that have been Merged for 30+ minutes and transitions them to Quality Control. It then adds a comment that tags the reporter to inform them that the issue is ready for review. """ issues = self.jira.search_issues( 'project=INDEXREP and status=Merged and updated<="-30m"') for issue in issues: reporter = issue.fields.reporter.key message = '[~%s], this issue is ready for QC.' % reporter """ 771 is the transition ID spedific to this step for this project. Anything more generic will need to parse the transitions list. """ self.jira.transition_issue(issue.key, '771') self.jira.add_comment(issue.key, message) def handle_audited_tickets(self): """ Handles audit tickets that are failed. Closed tickets are ignored. Failed tickets trigger the creation of a new ticket in the same project as the original ticket. Inputs: None Returns: None """ issues = self.jira.search_issues( # get all the ADT issues 'project=ADT and status="Failed Audit"') # For each failed issue, generate a new work ticket then close this one for issue in issues: #BUID adt_buid = issue.fields.customfield_10502 #WCID adt_wcid = issue.fields.customfield_10501 #Indexing Type adt_indexing_type = issue.fields.customfield_10500 #comments adt_comments = [] for comment in self.jira.comments(issue): node = { 'body': self.jira.comment(issue, comment).body, 'author': self.jira.comment(issue, comment).author.key } adt_comments.append(node) link_list = [ issue.key, ] # first linked ticket should be this audit ticket for link in issue.fields.issuelinks: # grab the rest of links try: link_list.append(link.outwardIssue.key) except AttributeError: pass # capture orignal tick and project original_ticket = issue.fields.summary.split("[")[1].split("]")[0] original_project = original_ticket.split("-")[0] # build the new summary by parsing the audit summary indexrep_summary = issue.fields.summary #build the summary indexrep_summary = indexrep_summary.replace( "compliance audit - ", "") indexrep_summary = indexrep_summary.split("[")[0] indexrep_summary = ' %s - Failed Audit' % (indexrep_summary) # Build the issue description message = 'This issue failed audit. Please review %s and make any \ necessary corrections.' % original_ticket # Construct the watcher list and de-dupe it watcher_list = [ issue.fields.assignee.key, ] for w in self.jira.watchers(issue).watchers: watcher_list.append(w.key) watcher_list = set(watcher_list) # get the reporter (reporter is preserved from audit to issue) reporter = issue.fields.reporter.key # Generate the new issue, then close the audit ticket. new_issue = self.make_new_issue(original_project, "EMPTY", reporter, indexrep_summary, message, watcher_list, link_list, adt_buid, adt_wcid, adt_indexing_type, adt_comments) close_me = self.close_issue(issue.key) def resolved_issue_audit(self, delay="", projects=""): """ TAKES issues that have been resolved from specified projectsfor a set interval and creates a new ticket in AUDIT, closes the INDEXREP ticket, and then assigns it to the audit user specified in the self.qa_auditor role. Inputs: :delay: how long an issue should be resoved before being picked up by this script. Defaults to class level variable :projects: which projects are subject to auditing. Defaults to class level variable Returns: Error message or Nothing """ delay = self.audit_delay if not delay else delay projects = self.audit_projects if not projects else projects # get all the issues from projects in the audit list issue_query = 'project in (%s) and status=Resolved and resolutiondate \ <="%s"' % (projects, delay) issues = self.jira.search_issues(issue_query) # get the users who can be assigned audit tickets. This should be just one person qa_members = self.get_group_members("issue audits") if len(qa_members) == 1: qa_auditor = qa_members.keys()[0] else: # for now, throw an error. Later, assign to user with fewer ADT tickets # this will also mean turning the code in auto_assign into a method (DRY) return "Error: There is more than one possible auditor" # cycle through them and create a new ADT ticket for each for issue in issues: #BUID ind_buid = issue.fields.customfield_10502 #WCID ind_wcid = issue.fields.customfield_10501 #Indexing Type ind_indexing_type = issue.fields.customfield_10500 link_list = [ issue.key, ] for link in issue.fields.issuelinks: # grab the rest of links try: link_list.append(link.outwardIssue.key) except AttributeError: pass # build the new ticket summary based on the issue being audited # [ISSUE=123] is used to preserve the original issue key. Replace any brackets with () # to prevent read errors later. adt_summary = issue.fields.summary.replace("[", "(").replace("]", ")") adt_summary = 'compliance audit - %s [%s]' % (adt_summary, issue.key) # build the description message = '[~%s], issue %s is ready to audit.' % (qa_auditor, issue.key) #build the watcher list, including original reporter and assignee of the audited ticket watcher_list = [] for w in self.jira.watchers(issue).watchers: watcher_list.append(w.key) reporter = issue.fields.reporter.key try: original_assignee = issue.fields.assignee.key except AttributeError: original_assignee = "EMPTY" # make the audit ticket new_issue = self.make_new_issue("ADT", qa_auditor, reporter, adt_summary, message, watcher_list, link_list, ind_buid, ind_wcid, ind_indexing_type) # close the INDEXREP ticket close_me = self.close_issue(issue.key) # add comment to indexrep ticket link_back_comment = "This issue has been closed. The audit ticket is %s" % new_issue self.jira.add_comment(issue.key, link_back_comment) def make_new_issue(self, project, issue_assignee, issue_reporter, summary, description="", watchers=[], links=[], buid="", wcid="", indexing_type="", comments=[], issuetype="Task"): """ Creates a new issue with the given parameters. Inputs: *REQUIRED* :project: the jira project key in which to create the issue :issue_assignee: user name who the issue will be assigned to :issue_reporter: user name of the issue report :summary: string value of the issue summary field *OPTIONAL* :description: Issue description. Defaults to empty string :watchers: list of user names to add as issue watchers :link: list of issue keys to link to the issue as "Related To" :issuetype: the type of issue to create. Defaults to type. :buid: business unit - custom field 10502 :wcid: wrapping company id - custom field 10501 :indexing_type: the indexing type - custom field 10500 :comments: list dictionaries of comments and authors to auto add. Returns: Jira Issue Object """ issue_dict = { 'project': { 'key': project }, 'summary': summary, 'issuetype': { 'name': issuetype }, 'description': description, } new_issue = self.jira.create_issue(fields=issue_dict) # assign the audit tick to auditor new_issue.update(assignee={'name': issue_assignee}) new_issue.update(reporter={'name': issue_reporter}) # add watchers to audit ticket (reporter, assignee, wacthers from indexrep ticket) for watcher in watchers: self.jira.add_watcher(new_issue, watcher) # link the audit ticket back to indexrep ticket for link in links: self.jira.create_issue_link('Relates', new_issue, link) # add custom field values if set if buid: new_issue.update(fields={'customfield_10502': buid}) if wcid: new_issue.update(fields={'customfield_10501': wcid}) if indexing_type: new_issue.update( fields={'customfield_10500': { 'value': indexing_type.value }}) # add comments quoted_comments = "" for comment in comments: quoted_comments = "%s[~%s] Said:{quote}%s{quote}\\\ \\\ " % ( quoted_comments, comment['author'], comment['body']) if quoted_comments: quoted_comments = "Comments from the parent issue:\\\ %s" % quoted_comments self.jira.add_comment(new_issue, quoted_comments) return new_issue # method to transistion audit ticket def get_group_members(self, group_name): """ Returns the members of a """ group = self.jira.groups(query=group_name)['groups'][0]['name'] members = self.jira.group_members(group) return members def auto_assign(self): """ Looks up new INDEXREP issues with an empty assignee and non-agent reporter and assigns them to the user in the content-acquisition user group with the fewest assigned contect-acquistion tickets. """ # filter 20702 returns issues that need to auto assigned jql_query = self.jira.filter("20702").jql issues = self.jira.search_issues(jql_query) #filter 21200 returns non-resolved assigned issues assigned_issues_query = self.jira.filter("21200").jql # cycle through each issue and assign it to the user in # content acquisition with the fewest assigned tickets for issue in issues: username = self.user_with_fewest_issues('content-acquisition', assigned_issues_query) reporter = issue.fields.reporter.key watch_list = self.toggle_watchers("remove", issue) self.jira.assign_issue(issue=issue, assignee=username) message = ("[~%s], this issue has been automically assigned " "to [~%s].") % (reporter, username) self.jira.add_comment(issue.key, message) self.toggle_watchers("add", issue, watch_list) def remind_reporter_to_close(self): """ Comments on all non-closed resolved issues that are 13 days without a change. Notifies the reporter it will be closed in 24 hours and adds a label to the issue that is used as a lookup key by the close method. """ issues = self.jira.search_issues('resolution != EMPTY AND \ status not in (closed, "Quality Control", Reopened, Merged, open) \ AND updated <= -13d and project not in (INDEXREP)') for issue in issues: reporter = issue.fields.reporter.key message = ( "[~%s], this issue has been resolved for 13 days. It will be " "closed automatically in 24 hours.") % reporter watch_list = self.toggle_watchers("remove", issue) self.jira.add_comment(issue.key, message) issue.fields.labels.append(self.ac_label) issue.update(fields={"labels": issue.fields.labels}) self.toggle_watchers("add", issue, watch_list) def close_resolved(self): """ Looks up all issues labeled for auto-closing that have not been updated in 24 hours and closes them. Ignores INDEXREP so as to not interfere with the auditing process. """ issues = self.jira.search_issues('resolution != EMPTY AND \ status not in (closed, "Quality Control", Reopened, Merged, open, \ passed,staged) AND project not in (INDEXREP) \ AND updated <= -24h \ AND labels in (auto-close-24-hours)') for issue in issues: reporter = issue.fields.reporter.key message = ( "[~%s], this issue has closed automatically.") % reporter close_me = self.close_issue(issue) self.jira.add_comment(issue.key, message) def close_issue(self, issue): """ Closes the issue passed to it with a resolution of fixed. Inputs: Issue: the issue object to close Returns: True|False """ trans = self.jira.transitions(issue) success_flag = False for tran in trans: tran_name = tran['name'].lower() if 'close' in tran_name or 'complete' in tran_name: try: self.jira.transition_issue(issue, tran['id'], {'resolution': { 'id': '1' }}) #some close transitions don't have a resolution screen except: #open ended, but the JIRAError exception is broken. self.jira.transition_issue(issue, tran['id']) success_flag = True return success_flag def clear_auto_close_label(self): """ Clears the auto-close label from issues that have been re-opened since the auto-close reminder was posted. """ issues = self.jira.search_issues( 'status in ("Quality Control", Reopened, Merged, open) \ AND labels in (auto-close-24-hours)') for issue in issues: label_list = issue.fields.labels watch_list = self.toggle_watchers("remove", issue) label_list.remove(self.ac_label) issue.update(fields={"labels": label_list}) self.toggle_watchers("add", issue, watch_list) def toggle_watchers(self, action, issue, watch_list=[]): """ Internal method that either adds or removes the watchers of an issue. If it removes them,it returns a list of users that were removed. If it adds, it returns an updated list of watchers. Inputs: :action: String "add"|"remove". The action to take :issue: Issue whose watchers list is being modified :watch_list: list of users. Optional for remove. Required for add. Returns: :issue_watcher: List of users who are or were watching the issue. """ if action == "remove": issue_watchers = self.jira.watchers(issue).watchers for issue_watcher in issue_watchers: self.jira.remove_watcher(issue, issue_watcher.name) else: for old_watcher in watch_list: self.jira.add_watcher(issue, old_watcher.name) issue_watchers = self.jira.watchers(issue).watchers return issue_watchers def label_contains(self, issue, search_string): """ Internal method that searches the labels of an issue for a given string value. It allows filtering that is roughly "labels ~ 'string'", which is not supported by JQL. Inputs: :issue: Jira issue object that is being checked :search_string: the string value being checked for Returns: True|False True if search_string exists in any label. """ return any(search_string in label for label in issue.fields.labels) def user_with_fewest_issues(self, group, query): """ Given a query, return the username of the use with the fewest assigned issues in the result set. Inputs: group: the group of users for which to count issues. query: the issues to lookup. Should be a JQL string. """ members = self.get_group_members(group) issues = self.jira.search_issues(query) member_count = {} for member in members: member_count[member] = 0 # perform the count anew for each ticket for issue in issues: if issue.fields.assignee: assignee = issue.fields.assignee.key else: assignee = None if assignee in members and not self.label_contains(issue, "wait"): member_count[assignee] = member_count[assignee] + 1 #sort the list so that the user with the lowest count is first member_count_sorted = sorted(member_count.items(), key=operator.itemgetter(1)) # return the username of the user return str(member_count_sorted[0][0])
if new_issue_label != "None": logger.debug('Adding label (' + new_issue_label + ') to (' + jira_issue.key + ')') syslog.syslog(syslog.LOG_DEBUG, 'Adding label (' + new_issue_label + ') to (' + jira_issue.key + ')') jira_issue.fields.labels.append(new_issue_label) jira_issue.update(fields={"labels": jira_issue.fields.labels}) # Once the JIRA ticket is created, should the security level be modified? if config.getboolean('jira', 'modify_security_level'): original_security_level = config.get('jira', 'original_security_level') original_security_level_mentioned = False if original_security_level != "None": if original_security_level in jira_issue.fields.description: original_security_level_mentioned = True else: # Obtain all current comments on the JIRA ticket. jira_comments = jira.comments(jira_issue) for existing_comment in jira_comments: if original_security_level in existing_comment.body: original_security_level_mentioned = True break if original_security_level_mentioned: logger.debug('Original security level for JIRA ticket (' + jira_issue.key + ') mentioned') syslog.syslog(syslog.LOG_DEBUG, 'Original security level for JIRA ticket (' + jira_issue.key + ') mentioned') # If checks passed, then modify the security level. if not original_security_level_mentioned: target_security_level = config.get('jira', 'target_security_level') if target_security_level == "None": logger.info('Removing security level for JIRA ticket (' + jira_issue.key + ')') syslog.syslog(syslog.LOG_INFO, 'Removing security level for JIRA ticket (' + jira_issue.key + ')')
class JiraService(IssueService): # A map of jira priorities to taskwarrior priorities priorities = { 'Trivial': 'L', 'Minor': 'L', 'Major': 'M', 'Critical': 'H', 'Blocker': 'H', } def __init__(self, *args, **kw): super(JiraService, self).__init__(*args, **kw) self.username = self.config.get(self.target, 'jira.username') self.url = self.config.get(self.target, 'jira.base_uri') password = self.config.get(self.target, 'jira.password') if not password or password.startswith("@oracle:"): service = "jira://%s@%s" % (self.username, self.url) password = get_service_password(service, self.username, oracle=password, interactive=self.config.interactive) default_query = 'assignee=' + self.username + \ ' AND status != closed and status != resolved' self.query = self.config.get(self.target, 'jira.query', default_query) self.project_prefix = self.config.get( self.target, 'jira.project_prefix', '') self.jira = JIRA( options={ 'server': self.config.get(self.target, 'jira.base_uri'), 'rest_api_version': 'latest', }, basic_auth=(self.username, password) ) @classmethod def validate_config(cls, config, target): for option in ['jira.username', 'jira.password', 'jira.base_uri']: if not config.has_option(target, option): die("[%s] has no '%s'" % (target, option)) IssueService.validate_config(config, target) def get_owner(self, issue): return True def annotations(self, issue): annotations = [] comments = self.jira.comments(issue) if comments is []: pass else: for comment in comments: created = date.fromtimestamp(time.mktime(time.strptime( comment.created[0:10], '%Y-%m-%d'))) annotations.append(self.format_annotation( created, comment.author.name, comment.body)) return dict(annotations) def __convert_for_jira4(self, issue): print(issue.key) class IssueWrapper: pass #print(self.jira.issue(issue.key).fields.summary.value) #print(self.jira.issue(issue.key).fields.summary) new_issue = self.jira.issue(issue.key) result = IssueWrapper() fields = IssueWrapper() fields.__dict__ = { 'summary': new_issue.fields.summary.value, 'priority': new_issue.fields.priority.name, } result.__dict__ = { 'key': issue.key, 'fields': fields, } return result def __issue(self, case, jira_version): result = dict( description=self.description( title=case.fields.summary, url=self.url + '/browse/' + case.key, number=case.key.rsplit('-', 1)[1], cls="issue"), project=self.project_prefix + case.key.rsplit('-', 1)[0], priority=self.priorities.get( get_priority(case.fields.priority), self.default_priority, ) ) if jira_version != 4: result.update(self.annotations(case.key)) return result def issues(self): cases = self.jira.search_issues(self.query, maxResults=-1) jira_version = 5 # Default version number if self.config.has_option(self.target, 'jira.version'): jira_version = self.config.getint(self.target, 'jira.version') if jira_version == 4: # Convert for older jira versions that don't support the new API cases = [self.__convert_for_jira4(case) for case in cases] log.name(self.target).debug(" Found {0} total.", len(cases)) return [self.__issue(case, jira_version) for case in cases]
class JeerahClient(object): def __init__(self, conf): self.conf = conf self.credentials = CredentialsConfig(self.conf.site.credentials) self.c = None self.issues_created = [] self.abort_on_error = True self.results_format = 'list' self.versions_cache = {} def connect(self): if self.c is None: self.c = JIRA(options={'server': self.conf.site.url}, basic_auth=(self.credentials.jira.username, self.credentials.jira.password)) logger.debug('created jira connection') else: logger.debug('jira connection exists') logger.debug('configured user: '******'actual user: '******'{0}' to version cache".format(ver.name)) if project not in self.versions_cache: self.versions_cache[project] = { } self.versions_cache[project][ver.name] = ver.id def create_issue(self, title, text, assignee, project, reporter=None, tags=None, version=None, uid=None): issue = {'project': {'key': project}, 'issuetype': {'name': 'Task'}, 'summary': title, 'description': text, 'assignee': {'name': assignee }} if report is not None: issue['reporter'] = { 'name': reporter } if tags is not None: issue['labels'] = tags if version is not None: if project not in self.versions_cache: logger.debug("updating version cache to include {0} versions".format(project)) self.update_version_cache(project) if version not in self.versions_cache[project]: logger.error("version {0} doesn't exist in {1} project".format(version, project)) else: issue['fixVersions'] = [ { 'id': self.versions_cache[project][version] } ] logger.debug('adding version to issue: {0}'.format(issue['fixVersions'])) new_issue = self.c.create_issue(fields=issue) logger.debug('created new issue {0}'.format(new_issue.key)) self.issues_created.append( { 'key': new_issue.key, 'uid': uid, 'title': title}) def query(self, query_string): logger.info('running query for: {0}'.format(query_string)) try: query_results = self.c.search_issues(jql_str=query_string, maxResults=200) except Exception as e: logger.warning(query_string) logger.error(e) if self.abort_on_error is True: raise SystemExit(e) if self.results_format == 'dict': return { issue.key: issue for issue in query_results } elif self.results_format == 'list': return [ issue for issue in query_results ] def versions(self, project, released=False, archived=False): return [ v for v in self.c.project_versions(project) if v.released is released and v.archived is archived ] def release_version(self, version): if not isinstance(version, Version): logger.error('{0} is not a jira version.'.format(version)) else: logger.info('releasing version {0}'.format(version.name)) version.update(released=True) def archive_version(self, version): if not isinstance(version, Version): logger.error('{0} is not a jira version.'.format(version)) else: logger.info('archiving version {0}'.format(version.name)) version.update(archived=True) def create_version(self, project, name, description='', release=None): if release is None: release = str(datetime.date.today() + datetime.timedelta(days=14)) elif release is False: release = None elif isinstance(release, datetime.date): release = str(release) self.c.create_version(name=name, project=project, description=description, releaseDate=release) logger.debug('created version {0} in project {0}'.format(name, project))
class JeerahClient(object): def __init__(self, conf): self.conf = conf self.credentials = giza.config.credentials.CredentialsConfig( self.conf.system.files.data.jira.site.credentials).jira self.c = None self.issues_created = [] self.abort_on_error = True self._results_format = 'list' self.versions_cache = {} @property def results_format(self): return self._results_format @results_format.setter def results_format(self, value): if value not in ("list", "dict"): m = "{0} is not in '{1}'", "value", ', '.join("list", "dict") logger.error(m) raise TypeError(m) else: self._results_format = value def connect(self): credentials = {} if self.credentials.access_token is not None: logger.debug('using OAuth') credentials['oauth'] = { 'access_token': self.credentials.access_token, 'access_token_secret': self.credentials.access_token_secret, 'consumer_key': self.credentials.consumer_key, 'key_cert': self.credentials.key_cert } elif self.credentials.username is not None: logger.debug('using basic authentication') credentials['basic_auth'] = (self.credentials.username, self.credentials.password) if self.c is None: self.c = JIRA(options={'server': self.credentials.url}, validate=True, **credentials) logger.debug('created jira connection') else: logger.debug('jira connection exists') def connect_unauthenticated(self): if self.c is None: self.c = JIRA(options={'server': self.conf.site.url}) logger.info("creating an unauthenticated jira connection.") def comments(self, issue): return self.c.comments(issue) def update_version_cache(self, project): versions = self.c.project_versions(project) for ver in versions: logger.debug("adding '{0}' to version cache".format(ver.name)) if project not in self.versions_cache: self.versions_cache[project] = {} self.versions_cache[project][ver.name] = ver.id def create_issue(self, title, text, assignee, project, reporter=None, tags=None, version=None, uid=None): issue = { 'project': { 'key': project }, 'issuetype': { 'name': 'Task' }, 'summary': title, 'description': text, 'assignee': { 'name': assignee } } if reporter is not None: issue['reporter'] = {'name': reporter} if tags is not None: issue['labels'] = [tags] if version is not None: if project not in self.versions_cache: logger.debug( "updating version cache to include {0} versions".format( project)) self.update_version_cache(project) if version not in self.versions_cache[project]: logger.error("version {0} doesn't exist in {1} project".format( version, project)) else: issue['fixVersions'] = [{ 'id': self.versions_cache[project][version] }] logger.debug('adding version to issue: {0}'.format( issue['fixVersions'])) new_issue = self.c.create_issue(fields=issue) logger.debug('created new issue {0}'.format(new_issue.key)) self.issues_created.append({ 'key': new_issue.key, 'uid': uid, 'title': title }) def query(self, query_string): logger.info('running query for: {0}'.format(query_string)) try: query_results = self.c.search_issues(jql_str=query_string, maxResults=200) except Exception as e: logger.warning(query_string) logger.error(e) if self.abort_on_error is True: raise SystemExit(e) if self.results_format == 'dict': return {issue.key: issue for issue in query_results} elif self.results_format == 'list': return [issue for issue in query_results] def components(self, project): return self.c.project_components(project) def versions(self, project, released=False, archived=False): return [ v for v in self.c.project_versions(project) if v.released is released and v.archived is archived ] def release_version(self, version): if not isinstance(version, Version): logger.error('{0} is not a jira version.'.format(version)) else: logger.info('releasing version {0}'.format(version.name)) version.update(released=True) def archive_version(self, version): if not isinstance(version, Version): logger.error('{0} is not a jira version.'.format(version)) else: logger.info('archiving version {0}'.format(version.name)) version.update(archived=True) def create_version(self, project, name, description='', release=None): if release is None: release = str(datetime.date.today() + datetime.timedelta(days=14)) elif release is False: release = None elif isinstance(release, datetime.date): release = str(release) self.c.create_version(name=name, project=project, description=description, releaseDate=release) logger.debug('created version {0} in project {0}'.format( name, project))
class JiraRestBridge(JiraBridge): def __init__(self, base_url, config, persist=False): super(JiraRestBridge, self).__init__(base_url, config, persist) self.jira = None @cached('resolutions') def get_resolutions(self): return dict((r.name, rest_recursive_dict(r.raw)) for r in self.jira.resolutions()) @cached('filters') def get_filters(self): filters = dict((f.name, rest_recursive_dict(f.raw)) for f in self.jira.favourite_filters()) return filters def clean_issue(self, issue): _issue = {} for k,v in issue.fields.__dict__.items(): if isinstance(v, Resource): _issue[k] = map_rest_resource(v) elif v is not None: _issue[k] = v _issue['key'] = issue.key _issue['type'] = map_rest_resource(_issue['issuetype']) return _issue def get_issue(self, issue_id): try: return self.clean_issue(self.jira.issue(issue_id)) except: return None def search_issues(self, free_text, project=None, limit=100): query = '(summary~"%s" or description~"%s")' % (free_text, free_text) if project: query += ' and project=%s' % project query += ' order by key' return [self.clean_issue(issue) for issue in self.jira.search_issues(query,maxResults=100)] def search_issues_jql(self, query, limit=100, project=None): return [self.clean_issue(issue) for issue in self.jira.search_issues(query, maxResults=100)] def get_issues_by_filter(self, *filters): return self.search_issues_jql( "filter in (%s)" % ",".join(['"%s"' % f for f in filters]) ) def add_comment(self, issue, comment): self.jira.add_comment(issue, comment) def transition_issue(self, issue, transition, comment=""): transitions = self.get_available_transitions(issue) try: return self.jira.transition_issue(issue, transitions[transition]['id']) except KeyError: raise JiraCliError("Invalid transition '%s'. Use one of [%s]" % (transition, ",".join(transitions))) def ping(self): return False def create_issue(self, project, type='bug', summary="", description="", priority="minor", parent=None): issue = { "project": {'key':project.upper()}, "summary": summary, "description": description, "priority": {'id':self.get_priorities()[priority.lower()]["id"]} } if type.lower() == 'epic': issue['customfield_11401'] = summary if parent: issue['issuetype'] = {'id':self.get_subtask_issue_types()[type.lower()]['id']} issue['parent'] = {'key':parent} else: issue['issuetype'] = {'id':self.get_issue_types()[type.lower()]['id']} return self.clean_issue(self.jira.create_issue(issue)) def login(self, username, password): try: self.jira = JIRA(options={'server': self.base_url}, basic_auth=(username, password), validate=True ) except JIRAError: raise JiraAuthenticationError('failure to authenticate') except RequestException: raise JiraInitializationError('failure to communicate with jira') def get_available_transitions(self, issue): return dict((t['name'].lower(), t) for t in self.jira.transitions(issue)) @cached('issue_types') def get_issue_types(self): types = dict((k.name.lower(), k.raw) for k in self.jira.issue_types() if not k.subtask) for k in types: if k=='id': types['id'] = types['id'][0] return types @cached('subtask_types') def get_subtask_issue_types(self): types = dict((k.name.lower(), k.raw) for k in self.jira.issue_types() if k.subtask) for k in types: if k=='id': types['id'] = types['id'][0] return types def update_issue(self, issue_id, **kwargs): pass @cached('projects') def get_projects(self): return dict((k.name.lower(), k.raw) for k in self.jira.projects()) @cached('priorities') def get_priorities(self): return dict((k.name.lower(), dict(k.raw)) for k in self.jira.priorities()) @cached('components') def get_components(self, project): return dict((k.name.lower(), dict(k.raw)) for k in self.jira.project_components(project)) @cached('statuses') def get_statuses(self): return dict((k.name.lower(), dict(k.raw)) for k in self.jira.statuses()) def get_issue_comments(self, issue): return [ dict(author=comment.author.name , body=comment.body , created=comment.created ) for comment in self.jira.comments(issue) ]
class JiraRestBridge(JiraBridge): def __init__(self, base_url, config, persist=False): super(JiraRestBridge, self).__init__(base_url, config, persist) self.jira = None @cached('resolutions') def get_resolutions(self): return dict((r.name.lower(), rest_recursive_dict(r.raw)) for r in self.jira.resolutions()) @cached('filters') def get_filters(self): filters = dict((f.name, rest_recursive_dict(f.raw)) for f in self.jira.favourite_filters()) return filters def clean_issue(self, issue): _issue = {} for k,v in issue.fields.__dict__.items(): if isinstance(v, Resource): _issue[k] = map_rest_resource(v) elif v is not None: _issue[k] = v _issue['key'] = issue.key _issue['type'] = map_rest_resource(_issue['issuetype']) return _issue def get_issue(self, issue_id): try: return self.clean_issue(self.jira.issue(issue_id)) except: return None def search_issues(self, free_text, project=None, limit=100): query = '(summary~"%s" or description~"%s")' % (free_text, free_text) if project: query += ' and project=%s' % project query += ' order by key' return [self.clean_issue(issue) for issue in self.jira.search_issues(query,maxResults=100)] def search_issues_jql(self, query, limit=100, project=None): return [self.clean_issue(issue) for issue in self.jira.search_issues(query, maxResults=100)] def get_issues_by_filter(self, *filters): return self.search_issues_jql( "filter in (%s)" % ",".join(['"%s"' % f for f in filters]) ) def add_comment(self, issue, comment): self.jira.add_comment(issue, comment) def transition_issue(self, issue, transition, resolution): transitions = self.get_available_transitions(issue) fields = {} if resolution: fields["resolution"] = self.get_resolutions()[resolution.lower()] try: return self.jira.transition_issue(issue, transitions[transition]['id'], fields=fields) except KeyError: raise JiraCliError("Invalid transition '%s'. Use one of [%s]" % (transition, ",".join(transitions))) def ping(self): return False def create_issue(self, project, type='bug', summary="", description="", priority="minor", parent=None, assignee="", reporter="", labels=[], components={}, **extras): issue = { "project": {'key':project.upper()}, "summary": summary, "description": description, "priority": {'id':self.get_priorities()[priority.lower()]["id"]}, "components": [{"name": k} for k in components.keys()] } if labels: issue['labels'] = labels if not issue["components"]: issue.pop("components") if type.lower() == 'epic': issue['customfield_11401'] = summary if parent: issue['issuetype'] = {'id':self.get_subtask_issue_types()[type.lower()]['id']} issue['parent'] = {'key':parent} else: issue['issuetype'] = {'id':self.get_issue_types()[type.lower()]['id']} if extras: issue.update(extras) issue = self.jira.create_issue(issue) if not (assignee or reporter): return self.clean_issue(issue) else: key = issue.key if assignee: issue = self.clean_issue(self.assign_issue(key, assignee)) if reporter: issue = self.clean_issue(self.change_reporter(key, reporter)) return issue def login(self, username, password): try: self.jira = JIRA(options={'server': self.base_url, 'check_update': False}, basic_auth=(username, password), get_server_info=False, validate=False ) except JIRAError: raise JiraAuthenticationError('failure to authenticate') except RequestException: raise JiraInitializationError('failure to communicate with jira') def get_available_transitions(self, issue): return dict((t['name'].lower(), t) for t in self.jira.transitions(issue)) @cached('issue_types') def get_issue_types(self): types = dict((k.name.lower(), k.raw) for k in self.jira.issue_types() if not k.subtask) for k in types: if k=='id': types['id'] = types['id'][0] return types @cached('subtask_types') def get_subtask_issue_types(self): types = dict((k.name.lower(), k.raw) for k in self.jira.issue_types() if k.subtask) for k in types: if k=='id': types['id'] = types['id'][0] return types def update_issue(self, issue_id, update={}, **kwargs): issue = self.jira.issue(issue_id) issue.update(update=update, **kwargs) return self.jira.issue(issue_id) def assign_issue(self, issue_id, assignee): return self.update_issue( issue_id, assignee={"name": assignee} ) def change_reporter(self, issue_id, reporter): return self.update_issue( issue_id, reporter={"name": reporter} ) def add_labels(self, issue_id, labels, merge=False): issue = self.jira.issue(issue_id) issue.fields.labels.extend(labels) return issue.update(fields={"labels": issue.fields.labels}) @cached('projects') def get_projects(self): return dict((k.name.lower(), k.raw) for k in self.jira.projects()) @cached('priorities') def get_priorities(self): return dict((k.name.lower(), dict(k.raw)) for k in self.jira.priorities()) @cached('components') def get_components(self, project): return [k.raw for k in self.jira.project_components(project)] def list_versions(self, project): versions = self.jira.project(project).versions versions.sort(cmp=lambda l, r: cmp(l.id, l.id)) return [k.raw for k in versions] @cached('statuses') def get_statuses(self): return dict((k.name.lower(), dict(k.raw)) for k in self.jira.statuses()) def get_issue_comments(self, issue): return [ dict(author=comment.author.name , body=comment.body , created=comment.created ) for comment in self.jira.comments(issue) ] def add_versions(self, issue, versions, type): args = {} if type == 'fix': args = {'fixVersions': [{"add": {"name": v}} for v in versions]} elif type == 'affects': args = {'versions': [{"add": {"name": v}} for v in versions]} return self.update_issue(issue, **args) def remove_versions(self, issue, versions, type): args = {} if type == 'fix': args = {'fixVersions': [{"remove": {"name": v}} for v in versions]} elif type == 'affects': args = {'versions': [{"remove": {"name": v}} for v in versions]} return self.update_issue(issue, **args)
class JiraRestBridge(JiraBridge): def __init__(self, base_url, config, persist=False): super(JiraRestBridge, self).__init__(base_url, config, persist) self.jira = None @cached('resolutions') def get_resolutions(self): return dict((r.name, rest_recursive_dict(r.raw)) for r in self.jira.resolutions()) @cached('filters') def get_filters(self): filters = dict((f.name, rest_recursive_dict(f.raw)) for f in self.jira.favourite_filters()) return filters def clean_issue(self, issue): _issue = {} for k, v in issue.fields.__dict__.items(): if isinstance(v, Resource): _issue[k] = map_rest_resource(v) elif v is not None: _issue[k] = v _issue['key'] = issue.key _issue['type'] = map_rest_resource(_issue['issuetype']) return _issue def get_issue(self, issue_id): try: return self.clean_issue(self.jira.issue(issue_id)) except: return None def search_issues(self, free_text, project=None, limit=100): query = '(summary~"%s" or description~"%s")' % (free_text, free_text) if project: query += ' and project=%s' % project query += ' order by key' return [ self.clean_issue(issue) for issue in self.jira.search_issues(query, maxResults=100) ] def search_issues_jql(self, query, limit=100, project=None): return [ self.clean_issue(issue) for issue in self.jira.search_issues(query, maxResults=100) ] def get_issues_by_filter(self, *filters): return self.search_issues_jql("filter in (%s)" % ",".join(['"%s"' % f for f in filters])) def add_comment(self, issue, comment): self.jira.add_comment(issue, comment) def transition_issue(self, issue, transition, comment=""): transitions = self.get_available_transitions(issue) try: return self.jira.transition_issue(issue, transitions[transition]['id']) except KeyError: raise JiraCliError("Invalid transition '%s'. Use one of [%s]" % (transition, ",".join(transitions))) def ping(self): return False def create_issue(self, project, type='bug', summary="", description="", priority="minor", parent=None, assignee="", reporter="", labels=[], components={}): issue = { "project": { 'key': project.upper() }, "summary": summary, "description": description, "priority": { 'id': self.get_priorities()[priority.lower()]["id"] }, "labels": labels, "components": [{ "name": k } for k in components.keys()] } if not issue["components"]: issue.pop("components") if type.lower() == 'epic': issue['customfield_11401'] = summary if parent: issue['issuetype'] = { 'id': self.get_subtask_issue_types()[type.lower()]['id'] } issue['parent'] = {'key': parent} else: issue['issuetype'] = { 'id': self.get_issue_types()[type.lower()]['id'] } issue = self.jira.create_issue(issue) if not (assignee or reporter): return self.clean_issue(issue) else: key = issue.key if assignee: issue = self.clean_issue(self.assign_issue(key, assignee)) if reporter: issue = self.clean_issue(self.change_reporter(key, reporter)) return issue def login(self, username, password): try: self.jira = JIRA(options={'server': self.base_url}, basic_auth=(username, password), validate=True) except JIRAError: raise JiraAuthenticationError('failure to authenticate') except RequestException: raise JiraInitializationError('failure to communicate with jira') def get_available_transitions(self, issue): return dict( (t['name'].lower(), t) for t in self.jira.transitions(issue)) @cached('issue_types') def get_issue_types(self): types = dict((k.name.lower(), k.raw) for k in self.jira.issue_types() if not k.subtask) for k in types: if k == 'id': types['id'] = types['id'][0] return types @cached('subtask_types') def get_subtask_issue_types(self): types = dict((k.name.lower(), k.raw) for k in self.jira.issue_types() if k.subtask) for k in types: if k == 'id': types['id'] = types['id'][0] return types def update_issue(self, issue_id, update={}, **kwargs): issue = self.jira.issue(issue_id) issue.update(update=update, **kwargs) return self.jira.issue(issue_id) def assign_issue(self, issue_id, assignee): return self.update_issue(issue_id, assignee={"name": assignee}) def change_reporter(self, issue_id, reporter): return self.update_issue(issue_id, reporter={"name": reporter}) def add_labels(self, issue_id, labels, merge=False): old_labels = [] if merge: issue = self.get_issue(issue_id) old_labels = issue.get("labels", []) return self.update_issue(issue_id, labels=old_labels + labels) @cached('projects') def get_projects(self): return dict((k.name.lower(), k.raw) for k in self.jira.projects()) @cached('priorities') def get_priorities(self): return dict( (k.name.lower(), dict(k.raw)) for k in self.jira.priorities()) @cached('components') def get_components(self, project): return [k.raw for k in self.jira.project_components(project)] @cached('statuses') def get_statuses(self): return dict( (k.name.lower(), dict(k.raw)) for k in self.jira.statuses()) def get_issue_comments(self, issue): return [ dict(author=comment.author.name, body=comment.body, created=comment.created) for comment in self.jira.comments(issue) ] def add_versions(self, issue, versions, type): args = {} if type == 'fix': args = {'fixVersions': [{"add": {"name": v}} for v in versions]} elif type == 'affects': args = {'versions': [{"add": {"name": v}} for v in versions]} return self.update_issue(issue, **args) def remove_versions(self, issue, versions, type): args = {} if type == 'fix': args = {'fixVersions': [{"remove": {"name": v}} for v in versions]} elif type == 'affects': args = {'versions': [{"remove": {"name": v}} for v in versions]} return self.update_issue(issue, **args)
class jira_api(): def __init__(self, server, user, pwd): """ :param server: jira域名 :param user:jira用户 :param pwd:jira账户密码 """ self.server = server; self.basic_auth = (user, pwd) self.jiraClient = None def login(self): self.jiraClient = JIRA(server=self.server, basic_auth=self.basic_auth) if self.jiraClient is not None: return True else: return False def get_issue_list(self,jql_str=None,page=0,limit=10,fields=None): """ 查询issue集合 :param jql_str: 查询语句 :param page: 分页参数 开始 :param limit: 分页参数 结束 :param fields: 查询字段 :return:issue: 列表 """ if self.jiraClient is None: self.login() if jql_str is None and jql_str == "": return [] return self.jiraClient.search_issues(jql_str=jql_str, startAt=page, maxResults=limit, fields=fields) def get_issue_info(self,id_or_key,fields=None): """ 根据id或key查询issue信息 :param id_or_key: issue id或key :param fields: 查询字段 :return: issue详细信息 """ if self.jiraClient is None: self.login() if id_or_key is None: return {} return self.jiraClient.issue(id_or_key, fields=fields) def get_comments_from_issue(self,issue): """ 获取issue中所有comment集合 :param issue: :return: """ if self.jiraClient is None: self.login() if issue is None: return [] return self.jiraClient.comments(issue) def get_comment_from_issue(self,issue,comment_id): if self.jiraClient is None: self.login() if issue is None: return {} if comment_id is None: return {} return self.jiraClient.comment(issue, comment_id) def add_comment_to_issue(self,issue_id,comment_str): if self.jiraClient is None: self.login() if issue_id is None or issue_id == "": return False if comment_str is None or comment_str == "": return False return self.jiraClient.add_comment(issue_id, comment_str) def update_comment(self,issue_id_or_key,comment_id,comment_str): if self.jiraClient is None: self.login() if issue_id_or_key is None or issue_id_or_key == "": return False if comment_id is None or comment_id == "": return False comment = self.get_comment_from_issue(self.get_issue_info(issue_id_or_key),comment_id) if comment is None: return False return comment.update(body=comment_str) def get_transitions_from_issue(self,issue): if self.jiraClient is None: self.login() if issue is None: return {} return self.jiraClient.transitions(issue) def assign_issue_to_user(self,issue_id,assignee): if self.jiraClient is None: self.login() if issue_id is None: return False if assignee is None: return False return self.jiraClient.assign_issue(issue_id, assignee) def transition_issues(self,issue_id,transition_id,assignee=None,comment=""): if self.jiraClient is None: self.login() if(issue_id is None): return False if(transition_id is None): return False if(comment is None): return False fields = None if assignee is not None and assignee != "": fields = {'assignee': {'name': assignee}} return self.jiraClient.transition_issue(issue=issue_id,fields=fields,transition=transition_id,comment=comment) def group_members(self,group_name): if self.jiraClient is None: self.login() if group_name is None: return [] return self.jiraClient.group_members(group_name) def get_project_by_key(self,key): if self.jiraClient is None: self.login() if key is None: return "" return self.jiraClient.project(key) def get_custom_field_option(self,id): if self.jiraClient is None: self.login() if id is None: return "" return self.jiraClient.custom_field_option(id)