예제 #1
0
def update_jira(issue_name, pull_url, user, action, auth):
    """ Update a Jira issue to attach a pull request. """
    jira = JIRA(options=JIRA_OPTIONS, basic_auth=auth)
    issue = jira.issue(issue_name)
    if action == 'opened' or action == 'reopened':
        if user in TRANSLATE_NAMES:
            user = TRANSLATE_NAMES['user']
        status = JIRA_URL + issue_name + '\n'
        if not issue.fields.status.name in ACTIVE_STATUS:
            status += 'Jira not updated (state was not active or new)'
        elif issue.fields.customfield_10010 != None:
            status += 'Jira not updated (pull request already registered)'
        elif issue.fields.assignee is not None and \
             issue.fields.assignee.name.lower() != user.lower():
            status += 'Jira not updated (user does not match)'
        else:
            if issue.fields.assignee is None:
                jira.assign_issue(issue, user)
            issue.update(fields={'customfield_10010': pull_url})
            if issue.fields.status.name != 'Active':
                jira.transition_issue(issue, '71')  # Assign and schedule
            jira.transition_issue(issue, '81')  # Attach Pull Request
            status += 'Jira updated'
    else:
        status = ''  # Don't know if it was closed for accept or reject...
    return status
예제 #2
0
def post_to_jira():

    jira=JIRA(basic_auth=(uid,jpwd),options={'server': 'https://jira.client.com','headers': {'X-Atlassian-Token': 'nocheck'}})
    
    workbook = xlrd.open_workbook(logname)
    worksheet = workbook.sheet_by_index(0)
    wb = copy(workbook)
    w_sheet = wb.get_sheet(0)

    num_rows = worksheet.nrows - 1
    num_cells = worksheet.ncols - 1
    curr_row = 0
    while curr_row < num_rows:
        curr_row+=1
        issue=jira.issue(worksheet.cell_value(curr_row, 0))
        curr_status = worksheet.cell_value(curr_row, 5)
        comment = worksheet.cell_value(curr_row, 6)
        spoolfile= working_dir+"\\"+worksheet.cell_value(curr_row,0)+"\\"+worksheet.cell_value(curr_row,4)
        # print spoolfile
        if curr_status=="DEPLOYED":
            jira.add_comment(issue,comment)            
            jira.add_attachment(issue,spoolfile)
            jira.assign_issue(issue,'sqadepartment')
            w_sheet.write(curr_row,5,"COMPLETED")
            webbrowser.open_new_tab('https://jira.client.com/browse/'+issue.key)
            
    wb.save(logname)
예제 #3
0
def resolve_ticket(ticket):
    print('[ ... ] Closing DESHELP-{}'.format(ticket))
    with open(app.config['ACCESS_PATH'], 'r') as cfile:
        conf = yaml.load(cfile)['jira']
    u = base64.b64decode(conf['uu']).decode().strip()
    p = base64.b64decode(conf['pp']).decode().strip()
    jira = JIRA(server='https://opensource.ncsa.illinois.edu/jira/',
                basic_auth=(u, p))
    j = jira.search_issues('key=DESHELP-{}'.format(ticket))[0]
    try:
        jira.assign_issue(j, 'desdm-wufoo')
        jira.add_comment(j, 'email sent')
        jira.transition_issue(j, '2')
    except:
        pass
예제 #4
0
def create_ticket(first, last, email, topics, subject, question):
    with open('config/desaccess.yaml', 'r') as cfile:
        conf = yaml.load(cfile, Loader=yaml.FullLoader)['jira']
    my_string_u = base64.b64decode(conf['uu']).decode().strip()
    my_string_p = base64.b64decode(conf['pp']).decode().strip()
    """
    This function creates the ticket coming form the help form
    """

    jira = JIRA(server="https://opensource.ncsa.illinois.edu/jira/",
                basic_auth=(my_string_u, my_string_p))

    body = """
    *ACTION ITEMS*
    - Please ASSIGN this ticket if it is unassigned.
    - PLEASE SEND AN EMAIL TO  *%s* to reply to this ticket
    - COPY the answer in the comments section and ideally further communication.
    - PLEASE close this ticket when resolved


    *Name*: %s %s

    *Email*: %s

    *Topics*:
    %s

    *Question*:
    {noformat}
    %s
    {noformat}

    """ % (email, first, last, email, topics, question)

    issue = {
        'project': {
            'key': 'DESRELEASE'
        },
        'issuetype': {
            'name': 'Task'
        },
        'summary': 'Q: %s' % subject,
        'description': body,
    }
    try:
        new_jira_issue = jira.create_issue(fields=issue)
        assignment_success = False
        try:
            assignment_success = jira.assign_issue(
                new_jira_issue, os.environ['JIRA_DEFAULT_ASSIGNEE'])
        except:
            pass
    except:
        new_jira_issue = 'JIRA ERROR'
    try:
        send_email(jira_issue=new_jira_issue, subject=subject, body=body)
    except:
        pass
예제 #5
0
class JiraRobot:
    ROBOT_LIBRARY_SCOPE = 'GLOBAL'
    jira = None

    def connect_to_jira(self,
                        JIRAUsername=None,
                        JIRAPassword=None,
                        options=None):
        """
        Connect to a JIRA server.

            Arguments:
                |  JIRAUsername (string)  	|  (Optional) A JIRA Username you wish to authenticate with, will authorise with anonymous account if left empty      				|
                |  JIRAPassword (string)  	|  (Optional) If a username was specified and a password is not the user will be prompted for password at runtime 					|
                |  options (dictionary) 	|  (Optional) A dictionary of options that the JIRA connection will be initialied with 	                                            |

            This must be called first to be able to do most JIRA actions such as creating a new issue and assigning users.
            When connecting to JIRA you may need to authenticate a user. This can be done by passing in Username and Password as parameters. However, should you wish to not have a sensitive password saved in a file,  an option is available to not pass in a Password and a prompt will appear asking for the JIRA password at runtime.


            'connect to jira' can be called on its own and will default options to:
                '{'rest_api_version': '2', 'verify': True,
                'server': 'http://*****:*****@ssword 						| {'server': http://devjira01'} 	|
        """

        if JIRAUsername is not None and (JIRAPassword is ""
                                         or JIRAPassword is None):
            JIRAPassword = getpass.getpass("\nJIRA Password: "******"\nAuthentication to JIRA unsuccessful. Ensure the user used has sufficient access and that Username and Password were correct\n\n"
                )
                sys.exit(1)

        else:
            try:
                self.jira = JIRA(options=JIRAOptions,
                                 basic_auth=(str(JIRAUsername),
                                             str(JIRAPassword)))
            except:
                sys.__stdout__.write(
                    "\nAuthentication to JIRA unsuccessful. Ensure the user used has sufficient access and that Username and Password were correct\n\n"
                )
                sys.exit(1)

    def create_issue(self, issue_field_dict, assign_current_user=False):
        """
        Creates a new JIRA issue.

            Arguments:
                |  issue_field_dict (string)  			| A dictionary in the form of a string that the user can specify the issues fields and field values 			|
                |  assign_current_user (string/bool)  	| (Optional) A flag to assign the current user to the issue once it is successfully created, defaults to False	|

            Will create a new issue and returns an Issue Key.
            The user will use the issue_field_dict variable to specify the issues field and their respective values. This must be in the form of a string written as a dictionary
                e.g. {'FieldName':'FieldValue'}

            The field value can also be another dictionary (in some field cases this is needed)
                e.g. {'FieldName1':{'key':'value'}, 'FieldName2':FieldValue, 'FieldName3':{'name':'value'}}

            This Create Issue Example page (https://developer.atlassian.com/display/JIRADEV/JIRA+REST+API+Example+-+Create+Issue) is full of useful information in what is required for creating issues, including customfields and issue field value types, it is very important to get the value type right or an error will be thrown.


            Examples:
                |  *Keyword*        	|  *Parameters*   	| 														| 		|
                |  ${issue_field_dict} 	|  {'project':{'key': 'PROJ'}, 'summary':'Create New Issue', 'description':'Creating a new issue', 'issuetype':{'name': 'Bug'}} |    |
                |  connect to jira      |  asimmons         | options= {'http://devjira01'}                         |       |
                |  ${issue}				|  create issue 	|  ${issue_field_dict}									|      	|

                |  connect to jira      |  asimmons         | options= {'http://devjira01'}                         |  		|
                |  ${issue}				|  create issue 	|  ${issue_field_dict}									|  True |
        """
        issue_field_dict = eval(str(issue_field_dict))
        print(issue_field_dict)

        new_issue = self.jira.create_issue(issue_field_dict)
        if assign_current_user is True:
            self.assign_user_to_issue(new_issue, self.jira.current_user())
        return new_issue

    def create_issue_link(self,
                          link_type,
                          inwardissue,
                          outwardissue,
                          comment=None):
        """
        Create a link between two issues.

            Arguments:
                |  link_type (string)  	| The type of link									|
                |  inwardissue (string)  	| The issue to link from  							|
                |  outwardissue (string)  	| The issue to link to 								|
                |  comment (string)  		| (Optional) A comment to add when joining issues	|

            Example:
                |  *Keyword*        	|  *Parameters* | 									| 			|
                |  connect to jira      |  asimmons     | options= {'http://devjira01'}     |  			|
                |  ${issue}				|  create issue |  ${issue_field_dict}				|  True 	|
                |  create issue link	|  relates to   |  ${issue} 						|  PROJ-385	|
        """
        self.jira.create_issue_link(type=link_type,
                                    inwardIssue=str(inwardissue),
                                    outwardIssue=str(outwardissue))

    def assign_user_to_issue(self, issue, JIRAUsername):
        # TODO: Review docs
        """
        Adds a user to a specified issue's watcher list

        Arguments:
            |  issue (string)  		| A JIRA Issue that a user needs to be assigned to, can be an issue ID or Key		|
            |  JIRAUsername (string)  	| A JIRA Username to assign a user to an issue   									|

        Example:
           |  *Keyword*        		|  *Parameters* | 									|
           |  connect to jira       |  asimmons     | options= {'http://devjira01'}     |
           |  ${issue}				|  create issue |  ${issue_field_dict}				|
           |  assign user to issue	|  ${issue}		|  aSample 							|
        """
        self.jira.assign_issue(issue=issue, assignee=JIRAUsername)

    def get_current_user(self):
        """
        Returns the current user used in the Connect to JIRA keyword.
        """
        return self.jira.current_user()

    def add_watcher_to_issue(self, issue, JIRAUsername):
        """
        Adds a user to a specified issue's watcher list.

        Arguments:
            |  issue (string)  		| A JIRA Issue that a watcher needs added to, can be an issue ID or Key		|
            |  JIRAUsername (string)  	| A JIRA Username to add as a watcher to an issue   					|

        Example:
            |  *Keyword*        	|  *Parameters* | 								|		|
            |  connect to jira  |  asimmons     | options= {'http://devjira01'}     | 		|
            |  ${issue}				|  create issue |  ${issue_field_dict}			|  True |
            |  add watcher to issue	|  ${issue}		|  aSample 						| 		|

        """
        self.jira.add_watcher(issue=issue, watcher=JIRAUsername)

    def add_comment_to_issue(self, issue, comment, visibility=None):
        """
        Adds a comment to a specified issue from the current user.

            Arguments:
                |  issue (string)  		| A JIRA Issue that a watcher needs added to, can be an issue ID or Key	|
                |  comment (string)  		| A body of text to add as a comment to an issue   						|
                |  visibility (string)  	| (Optional)															|

            Example:
                |  *Keyword*        	|  *Parameters* | 									|		|
                |  connect to jira      |  asimmons     |  options= {'http://devjira01'}    | 		|
                |  ${issue}				|  create issue |  ${issue_field_dict}				|  True |
                |  add comment to issue	|  ${issue}		|  Starting work on this issue		| 		|
        """
        self.jira.add_comment(issue=issue, body=comment)

    def add_attachment_to_issue(self, issue, attachment, filename=None):
        """
        Uploads and attaches a file a specified issue. (Note: include the file extention when using the 'filename' option or this will change the file type.)

        Arguments:
            |   issue (string)  	| A JIRA Issue that a watcher needs added to, can be an issue ID or Key		|
            |   attachment (string) | A string pointing to a file location to upload and attach to the issue	|
            |   filename (string)  	| (Optional) A string to rename the file to upon attaching to the issue  	|

        Example:
            |  *Keyword*        		|  *Parameters* | 							         |						|
            |  connect to jira          |  asimmons     | options= {'http://devjira01'}      | 						|
            |  ${issue}					|  create issue |  ${issue_field_dict}	             |  True 			 	|
            |  add attchment to issue	|  ${issue}		|  ./logfile.text			         |  LogInformation.txt	|
        """
        self.jira.add_attachment(issue=issue,
                                 attachment=attachment,
                                 filename=filename)
예제 #6
0
class Brewery:
    """Workflow Wrapper"""
    def __init__(self, config_file_path, session_file_path, repo=None):
        self._gerrit_url = None
        self._jira = None
        self._repo = repo

        if os.path.isfile(session_file_path):
            with open(session_file_path, 'rb') as session_file:
                session = pickle.load(session_file)
        else:
            session = None

        if config_file_path is not None:
            self._read_config(config_file_path, session)

        if session is not None:
            session.max_retries = 3
            self._jira._session = session
        else:
            with open(session_file_path, 'wb') as session_file:
                pickle.dump(self._jira._session, session_file)

    def _read_config(self, config_file_path, session):
        config = ConfigParser.ConfigParser()
        config.read(config_file_path)

        if len(config.sections()) == 0:
            # New config file
            pass

        # Will terminate if missing configs.
        Brewery._have_required_configs(config)

        self._gerrit_url = urlparse.urlparse(config.get('Gerrit', 'url'),
                                             'ssh')

        if session is not None:
            self._jira = JIRA(server=config.get('JIRA', 'server'))
        else:
            self._jira = JIRA(server=config.get('JIRA', 'server'),
                              basic_auth=(config.get('JIRA', 'username'),
                                          config.get('JIRA', 'password')))

    def work_on(self,
                issue_id=None,
                issue_type='Bug',
                summary=None,
                description=None):
        if issue_id is not None:
            issue = self._jira.issue(issue_id)
        else:
            issue = self._jira.create_issue(
                project={'key': self._get_project_key()},
                summary=summary,
                description=description,
                issuetype={'name': issue_type})

        issue_id = issue.key
        # Make sure its assigned to you
        self._jira.assign_issue(issue, self._jira.current_user())
        transitions = self._jira.transitions(issue_id)
        # If not already in progress, transition it.
        for transition in transitions:
            if 'Start' in transition['name']:
                self._jira.transition_issue(issue_id, transition['id'])

        if issue_id not in self._repo.heads:
            self._repo.create_head(issue_id)
            self._repo.heads[issue_id].checkout()
            self._repo.index.commit('%s. %s' %
                                    (issue_id, issue.fields.summary))
        else:
            self._repo.heads[issue_id].checkout()

    def post_review(self, draft=False, reviewers=None, target_branch='master'):
        ref = 'drafts' if draft else 'for'
        origin = self._repo.remote('origin')
        refspec = 'HEAD:refs/%s/%s' % (ref, target_branch)
        if reviewers is not None:
            refspec = '%s%%r=%s' % (refspec, ",r=".join(reviewers.split(",")))
        res = origin.push(refspec=refspec)
        if len(res) != 1:
            click.echo('Failed to execute git push to post review.')
        else:
            push_info = res[0]
            if push_info.flags & push_info.ERROR != 0:
                click.echo('Failed to post review because %s' %
                           push_info.summary)
            else:
                click.echo('Pushed review for %s. Draft=[%s]' %
                           (self._repo.active_branch.name, draft))

    def submit(self):
        commit_sha = self._repo.active_branch.commit.hexsha
        issue_id = self._repo.active_branch.name
        cmd = [
            'ssh', '-p',
            str(self._gerrit_url.port),
            '%s@%s' % (self._gerrit_url.username, self._gerrit_url.hostname),
            'gerrit', 'review', '--submit', commit_sha
        ]
        try:
            subprocess.check_call(cmd)

            transitions = self._jira.transitions(issue_id)
            # If not already in progress, transition it.
            for transition in transitions:
                if 'Fixed' in transition['name']:
                    self._jira.transition_issue(issue_id, transition['id'])
        except Exception as e:
            click.echo('Failed to merge review: %s' % e.message)

    def _get_project_key(self):
        max_tries = 5
        ref = 0
        while ref < max_tries:
            commit = self._repo.commit("HEAD~%s" % ref)
            ref += 1
            # No match returns None
            match = re.search('^([a-zA-Z]{3})(-[0-9]+)', commit.message)
            if match is not None:
                return match.group(1)

        return None

    @staticmethod
    def _have_required_configs(config):
        """Will raise an exception if any are missing."""
        config.get('JIRA', 'server')
        config.get('JIRA', 'username')
        config.get('JIRA', 'password')
        config.get('Gerrit', 'url')

    def list_projects(self):
        for project in self._jira.projects():
            click.echo('%s - %s' % (project.key, project.name))
예제 #7
0
def ReAssignJiraTicket(jiraName, assignID):
    jira = JIRA(server, basic_auth=(username, password))
    issue = jira.issue(jiraName)
    jira.assign_issue(issue, assignID)
    return
예제 #8
0
class Brewery:
    """Workflow Wrapper"""

    def __init__(self, config_file_path, session_file_path, repo=None):
        self._gerrit_url = None
        self._jira = None
        self._repo = repo

        if os.path.isfile(session_file_path):
            with open(session_file_path, 'rb') as session_file:
                session = pickle.load(session_file)
        else:
            session = None

        if config_file_path is not None:
            self._read_config(config_file_path, session)

        if session is not None:
            session.max_retries = 3
            self._jira._session = session
        else:
            with open(session_file_path, 'wb') as session_file:
                pickle.dump(self._jira._session, session_file)

    def _read_config(self, config_file_path, session):
        config = ConfigParser.ConfigParser()
        config.read(config_file_path)

        if len(config.sections()) == 0:
            # New config file
            pass

        # Will terminate if missing configs.
        Brewery._have_required_configs(config)

        self._gerrit_url = urlparse.urlparse(config.get('Gerrit', 'url'), 'ssh')

        if session is not None:
            self._jira = JIRA(server=config.get('JIRA', 'server'))
        else:
            self._jira = JIRA(
                server=config.get('JIRA', 'server'),
                basic_auth=(config.get('JIRA', 'username'), config.get('JIRA', 'password'))
            )

    def work_on(self, issue_id=None, issue_type='Bug', summary=None, description=None):
        if issue_id is not None:
            issue = self._jira.issue(issue_id)
        else:
            issue = self._jira.create_issue(
                project={'key': self._get_project_key()},
                summary=summary,
                description=description,
                issuetype={'name': issue_type}
            )

        issue_id = issue.key
        # Make sure its assigned to you
        self._jira.assign_issue(issue, self._jira.current_user())
        transitions = self._jira.transitions(issue_id)
        # If not already in progress, transition it.
        for transition in transitions:
            if 'Start' in transition['name']:
                self._jira.transition_issue(issue_id, transition['id'])

        if issue_id not in self._repo.heads:
            self._repo.create_head(issue_id)
            self._repo.heads[issue_id].checkout()
            self._repo.index.commit('%s. %s' % (issue_id, issue.fields.summary))
        else:
            self._repo.heads[issue_id].checkout()

    def post_review(self, draft=False, reviewers=None, target_branch='master'):
        ref = 'drafts' if draft else 'for'
        origin = self._repo.remote('origin')
        refspec = 'HEAD:refs/%s/%s' % (ref, target_branch)
        if reviewers is not None:
            refspec = '%s%%r=%s' % (refspec, ",r=".join(reviewers.split(",")))
        res = origin.push(refspec=refspec)
        if len(res) != 1:
            click.echo('Failed to execute git push to post review.')
        else:
            push_info = res[0]
            if push_info.flags & push_info.ERROR != 0:
                click.echo('Failed to post review because %s' % push_info.summary)
            else:
                click.echo('Pushed review for %s. Draft=[%s]' % (self._repo.active_branch.name, draft))

    def submit(self):
        commit_sha = self._repo.active_branch.commit.hexsha
        issue_id = self._repo.active_branch.name
        cmd = [
            'ssh',
            '-p',
            str(self._gerrit_url.port),
            '%s@%s' % (self._gerrit_url.username, self._gerrit_url.hostname),
            'gerrit',
            'review',
            '--submit',
            commit_sha
        ]
        try:
            subprocess.check_call(cmd)

            transitions = self._jira.transitions(issue_id)
            # If not already in progress, transition it.
            for transition in transitions:
                if 'Fixed' in transition['name']:
                    self._jira.transition_issue(issue_id, transition['id'])
        except Exception as e:
            click.echo('Failed to merge review: %s' % e.message)

    def _get_project_key(self):
        max_tries = 5
        ref = 0
        while ref < max_tries:
            commit = self._repo.commit("HEAD~%s" % ref)
            ref += 1
            # No match returns None
            match = re.search('^([a-zA-Z]{3})(-[0-9]+)', commit.message)
            if match is not None:
                return match.group(1)

        return None

    @staticmethod
    def _have_required_configs(config):
        """Will raise an exception if any are missing."""
        config.get('JIRA', 'server')
        config.get('JIRA', 'username')
        config.get('JIRA', 'password')
        config.get('Gerrit', 'url')

    def list_projects(self):
        for project in self._jira.projects():
            click.echo('%s - %s' % (project.key, project.name))
예제 #9
0
class JiraClient(object):
    def __init__(self, server, username, password, default_project=None):
        self.username = username
        self.password = password
        self.default_project = default_project
        self.server = server

        self.jira = JIRA(options={'server': self.server},
                         basic_auth=(self.username, self.password))

    def my(self, **kwargs):
        self._list_my_issues(**kwargs)

    def issue(self, **kwargs):
        issue_id = kwargs['issue_id']
        issue = self.jira.issue(issue_id)

        print 'ID: %s' % issue.key
        print 'URL: %s/browse/%s' % (self.server, issue.key)
        print 'Assignee: %s' % issue.fields.assignee.name
        print 'Status: %s' % issue.fields.status.name
        print ''
        print 'Summary: %s' % issue.fields.summary
        print ''
        print 'Details: %s' % issue.fields.description

    def log(self, **kwargs):
        issue_id = kwargs['issue_id']
        issue = self.jira.issue(issue_id)

        time_spent = kwargs['time']
        msg = kwargs['message']

        #FIXME: add comment as a part of worklog (investigate API)
        self.jira.add_comment(issue, msg)
        self.jira.add_worklog(issue, time_spent)
        print 'Your worklog was saved'

    def resolve(self, **kwargs):
        issue_id = kwargs['issue_id']
        issue = self.jira.issue(issue_id)

        self.jira.transition_issue(issue,
                                   '5',
                                   resolution={'name': 'Implemented'})
        print 'Issue %s was resolved as implemented' % issue_id

    def assign(self, **kwargs):
        issue_id = kwargs['issue_id']
        issue = self.jira.issue(issue_id)

        self.jira.assign_issue(issue, self.username)
        print 'Issue %s was assigned to %s' % (issue_id, self.username)
        if kwargs['start']:
            self.jira.transition_issue(issue, '4')
            print 'Issue %s was started' % issue_id

    def todo(self, **kwargs):
        query_parts = ['status = Open', 'type != Story', 'created >= "-14d"']

        if kwargs['project'] is not None:
            query_parts.append('project=%s' % kwargs['project'])
        elif self.default_project is not None:
            query_parts.append('project=%s' % self.default_project)

        query = ' and '.join(query_parts)
        self._perform_and_print_query(query)

    def _list_my_issues(self, **kwargs):
        query_parts = ['assignee=%s' % (self.username)]

        if kwargs['project'] is not None:
            query_parts.append('project=%s' % kwargs['project'])
        elif self.default_project is not None:
            query_parts.append('project=%s' % self.default_project)

        if kwargs['unresolved']:
            query_parts.append('resolution=unresolved')

        query = ' and '.join(query_parts)
        self._perform_and_print_query(query)

    def _perform_and_print_query(self, query):
        my_issues = self.jira.search_issues(query)

        for issue in my_issues:
            print '%s\t%s' % (issue.key, issue.fields.summary)
예제 #10
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]) 
예제 #11
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])
예제 #12
0
class Tasks(object):
    def __init__(self, configfile='bamboo.cfg'):
        self.jira_user = '******'
        self.jira_password = '******'
        self.server_name = 'https://jira.rutube.ru'
        parse_config(self, configfile)
        self.jira = JIRA({'server': self.server_name},
                         basic_auth=(self.jira_user, self.jira_password))

    def get_versions(self, task_key):
        self.issue = self.jira.issue(task_key)
        result = []
        for v in self.issue.fields.fixVersions:
            if v.archived or v.released:
                continue
            version = v.name
            if not re.match(r'^[\d]+\.[\d]+\.[\d]+$', version):
                continue
            result.append(version)
        return result

    def get_transitions(self, task_key):
        return self.jira.transitions(task_key)

    def search_tasks(self,
                     project_key,
                     status=None,
                     issue_type=None,
                     assignee=None,
                     release=None):
        query = "project = %s" % project_key
        if isinstance(status, (tuple, list)):
            statuses = ', '.join('"%s"' % s for s in status)
            query += ' AND status IN (%s)' % statuses
        if isinstance(status, str):
            query += ' AND status = "%s"' % status
        if isinstance(issue_type, (tuple, list)):
            types = ', '.join('"%s"' % t for t in issue_type)
            query += ' AND type IN (%s)' % types
        if isinstance(issue_type, str):
            query += ' AND type = "%s"' % issue_type
        if assignee:
            if assignee == 'currentUser()':
                query += ' AND assignee=currentUser()'
            else:
                query += ' AND assignee="%s"' % assignee
        if release:
            query += ' AND fixVersion="%s"' % release
        return self.jira.search_issues(query)

    def transition(self, task_key, transition_id):
        self.jira.transition_issue(task_key, transition_id)

    def assign(self, task_key, assignee):
        self.jira.assign_issue(task_key, assignee)

    def get_assignee(self, task_key):
        issue = self.jira.issue(task_key)
        return issue.fields.assignee.name

    def task_info(self, task_key):
        issue = self.jira.issue(task_key)
        result = (
            ('key', issue.key),
            ('title', issue.fields.summary),
            ('assignee', issue.fields.assignee.name),
            ('status', issue.fields.status.name),
        )
        return result

    def move(self, task_key, transition_name):
        transition_name = transition_name.lower().replace(' ', '-')
        transitions = self.get_transitions(task_key)
        for trans in transitions:
            name = trans['to']['name'].lower().replace(' ', '-')
            if name == transition_name:
                self.transition(task_key, trans['id'])
                return True
        return False
예제 #13
0
class Jira(object):
    '''
    Encapsulates interaction with Jira server.
    '''

    def __init__(self, config):
        self.config = config
        self.options = {'server': config['server']}
        self.server = JIRA(self.options,
                basic_auth=(config['username'], config['password']))

    def query(self, jql_query):
        '''
        Run a JQL query.
        '''
        return self.server.search_issues(jql_query)

    def create_issue(self, summary, description, kind='Bug'):
        '''
        Create a Jira issue -- defaults to a bug.
        '''

        new_issue = self.server.create_issue(
            project={'key': self.config['project']},
            summary=summary,
            description=summary,
#       components=[{'id': '10301', 'name': 'Server Engineering'}],
            assignee={'name': self.config['username']},
            issuetype={'name': kind})

        return new_issue

    def close_issue(self, ticket):
        issue = self.get_issue(ticket)
        transitions = self.server.transitions(issue)
        transition_names = [x['name'] for x in transitions]
        if 'Stop Progress' in transition_names:
            self.transition_issue(ticket, status='Stop Progress')
            transitions = self.server.transitions(issue)
            transition_names = [x['name'] for x in transitions]

        if 'Resolve Issue' in transition_names:
            self.transition_issue(ticket, status='Resolve Issue')
            return self.get_issue(ticket)
        else:
            raise InvalidJiraStatusException(
                    '\nattempt to close ticket failed. \
            \nValid transitions: {0}'.format(', '.join(transition_names)))

    def transition_issue(self, ticket, status='Resolve Issue'):

        issue = self.get_issue(ticket)
        transitions = self.server.transitions(issue)
        transition_names = [x['name'] for x in transitions]
        if status not in transition_names:
            raise InvalidJiraStatusException(
            '\n\'{0}\' is not a valid status for this Jira issue. \
            \nValid transitions: {1}'.format(
                status, ', '.join(transition_names)))
        _id = [x['id'] for x in transitions if x['name'] == status][0]
        # transition it:
        self.server.transition_issue(issue, _id)
        return self.get_issue(ticket)

#users = self.jira.server.search_assignable_users_for_issues('',
#        project='CIGNAINC', maxResults=500)

    def add_watcher(self, ticket, person):

        issue = self.get_issue(ticket)
        self.server.add_watcher(issue, person)
        return issue

    def list_reviewable(self):
        '''
        if a JQL query is defined in the config use it.  If not:
            if a named filter is defined in the config use it.  If not:
                use the predefined filter.
        '''

        section = self.config.get('review', None)
        jql = 'status IN ("Ready for Review", "In Review") \
            ORDER BY priority, updatedDate ASC'

        if not section:
            return self.query(jql)

        jql_config = section.get('jql', None)
        if jql_config:
            return self.query(jql_config)

        filter_name = section.get('filter', None)

        if filter_name:
            filters = self.server.favourite_filters()
            jql = [x.jql for x in filters if x.name == filter_name][0]
            return self.query(jql)

        return self.query(jql)

    def list_issues(self):
        '''
        if a JQL query is defined in the config use it.  If not:
            if a named filter is defined in the config use it.  If not:
                use the predefined filter.
        '''

        section = self.config.get('list', None)
        jql = 'assignee=currentUser() \
                AND status != Closed AND status != Resolved'

        if not section:
            return self.query(jql)

        jql_config = section.get('jql', None)
        if jql_config:
            return self.query(jql_config)

        filter_name = section.get('filter', None)

        if filter_name:
            filters = self.server.favourite_filters()
            jql = [x.jql for x in filters if x.name == filter_name][0]
            return self.query(jql)

        return self.query(jql)

    def delete_issue(self, ticket):

        issue = self.get_issue(ticket)
        issue.delete()

    def assign_issue(self, ticket, assignee):
        '''
        Given an issue and an assignee, assigns the issue to the assignee.
        '''
        issue = self.get_issue(ticket)
        self.server.assign_issue(issue, assignee)
        return issue

    def add_comment(self, ticket, comment):
        '''
        Given an issue and a comment, adds the comment to the issue.
        '''
        issue = self.get_issue(ticket)
        self.server.add_comment(issue, comment)
        return issue

    def get_issue(self, ticket):
        '''
        Given an issue name, returns a Jira instance of that issue.
        '''
        if isinstance(ticket, Issue):
            ticket = ticket.key

        return self.server.issue('{0}'.format(ticket))
예제 #14
0
class JiraRobot:
    ROBOT_LIBRARY_SCOPE = 'GLOBAL'
    jira = None

    def connect_to_jira(self, JIRAUsername=None,
                        JIRAPassword=None, options=None):
        """
        Connect to a JIRA server.

            Arguments:
                |  JIRAUsername (string)  	|  (Optional) A JIRA Username you wish to authenticate with, will authorise with anonymous account if left empty      				|
                |  JIRAPassword (string)  	|  (Optional) If a username was specified and a password is not the user will be prompted for password at runtime 					|
                |  options (dictionary) 	|  (Optional) A dictionary of options that the JIRA connection will be initialied with 	                                            |

            This must be called first to be able to do most JIRA actions such as creating a new issue and assigning users.
            When connecting to JIRA you may need to authenticate a user. This can be done by passing in Username and Password as parameters. However, should you wish to not have a sensitive password saved in a file,  an option is available to not pass in a Password and a prompt will appear asking for the JIRA password at runtime.


            'connect to jira' can be called on its own and will default options to:
                '{'rest_api_version': '2', 'verify': True,
                'server': 'http://*****:*****@ssword 						| {'server': http://devjira01'} 	|
        """

        if JIRAUsername is not None and (JIRAPassword is "" or JIRAPassword is None):
            JIRAPassword = getpass.getpass("\nJIRA Password: "******"\nAuthentication to JIRA unsuccessful. Ensure the user used has sufficient access and that Username and Password were correct\n\n")
                sys.exit(1)

        else:
            try:
                self.jira = JIRA(options=JIRAOptions, basic_auth=(str(JIRAUsername),
                                 str(JIRAPassword)))
            except:
                sys.__stdout__.write("\nAuthentication to JIRA unsuccessful. Ensure the user used has sufficient access and that Username and Password were correct\n\n")
                sys.exit(1)

    def create_issue(self, issue_field_dict, assign_current_user=False):
        """
        Creates a new JIRA issue.

            Arguments:
                |  issue_field_dict (string)  			| A dictionary in the form of a string that the user can specify the issues fields and field values 			|
                |  assign_current_user (string/bool)  	| (Optional) A flag to assign the current user to the issue once it is successfully created, defaults to False	|

            Will create a new issue and returns an Issue Key.
            The user will use the issue_field_dict variable to specify the issues field and their respective values. This must be in the form of a string written as a dictionary
                e.g. {'FieldName':'FieldValue'}

            The field value can also be another dictionary (in some field cases this is needed)
                e.g. {'FieldName1':{'key':'value'}, 'FieldName2':FieldValue, 'FieldName3':{'name':'value'}}

            This Create Issue Example page (https://developer.atlassian.com/display/JIRADEV/JIRA+REST+API+Example+-+Create+Issue) is full of useful information in what is required for creating issues, including customfields and issue field value types, it is very important to get the value type right or an error will be thrown.


            Examples:
                |  *Keyword*        	|  *Parameters*   	| 														| 		|
                |  ${issue_field_dict} 	|  {'project':{'key': 'PROJ'}, 'summary':'Create New Issue', 'description':'Creating a new issue', 'issuetype':{'name': 'Bug'}} |    |
                |  connect to jira      |  asimmons         | options= {'http://devjira01'}                         |       |
                |  ${issue}				|  create issue 	|  ${issue_field_dict}									|      	|

                |  connect to jira      |  asimmons         | options= {'http://devjira01'}                         |  		|
                |  ${issue}				|  create issue 	|  ${issue_field_dict}									|  True |
        """
        issue_field_dict = eval(str(issue_field_dict))
        print issue_field_dict

        new_issue = self.jira.create_issue(issue_field_dict)
        if assign_current_user is True:
            self.assign_user_to_issue(new_issue, self.jira.current_user())
        return new_issue

    def create_issue_link(self, link_type, inwardissue,
                          outwardissue, comment=None):
        """
        Create a link between two issues.

            Arguments:
                |  link_type (string)  	| The type of link									|
                |  inwardissue (string)  	| The issue to link from  							|
                |  outwardissue (string)  	| The issue to link to 								|
                |  comment (string)  		| (Optional) A comment to add when joining issues	|

            Example:
                |  *Keyword*        	|  *Parameters* | 									| 			|
                |  connect to jira      |  asimmons     | options= {'http://devjira01'}     |  			|
                |  ${issue}				|  create issue |  ${issue_field_dict}				|  True 	|
                |  create issue link	|  relates to   |  ${issue} 						|  PROJ-385	|
        """
        self.jira.create_issue_link(type=link_type,
                                    inwardIssue=str(inwardissue),
                                    outwardIssue=str(outwardissue))

    def assign_user_to_issue(self, issue, JIRAUsername):
    # TODO: Review docs
        """
        Adds a user to a specified issue's watcher list

        Arguments:
            |  issue (string)  		| A JIRA Issue that a user needs to be assigned to, can be an issue ID or Key		|
            |  JIRAUsername (string)  	| A JIRA Username to assign a user to an issue   									|

        Example:
           |  *Keyword*        		|  *Parameters* | 									|
           |  connect to jira       |  asimmons     | options= {'http://devjira01'}     |
           |  ${issue}				|  create issue |  ${issue_field_dict}				|
           |  assign user to issue	|  ${issue}		|  aSample 							|
        """
        self.jira.assign_issue(issue=issue, assignee=JIRAUsername)

    def get_current_user(self):
        """
        Returns the current user used in the Connect to JIRA keyword.
        """
        return self.jira.current_user()

    def add_watcher_to_issue(self, issue, JIRAUsername):
        """
        Adds a user to a specified issue's watcher list.

        Arguments:
            |  issue (string)  		| A JIRA Issue that a watcher needs added to, can be an issue ID or Key		|
            |  JIRAUsername (string)  	| A JIRA Username to add as a watcher to an issue   					|

        Example:
            |  *Keyword*        	|  *Parameters* | 								|		|
            |  connect to jira  |  asimmons     | options= {'http://devjira01'}     | 		|
            |  ${issue}				|  create issue |  ${issue_field_dict}			|  True |
            |  add watcher to issue	|  ${issue}		|  aSample 						| 		|

        """
        self.jira.add_watcher(issue=issue, watcher=JIRAUsername)

    def add_comment_to_issue(self, issue, comment, visibility=None):
        """
        Adds a comment to a specified issue from the current user.

            Arguments:
                |  issue (string)  		| A JIRA Issue that a watcher needs added to, can be an issue ID or Key	|
                |  comment (string)  		| A body of text to add as a comment to an issue   						|
                |  visibility (string)  	| (Optional)															|

            Example:
                |  *Keyword*        	|  *Parameters* | 									|		|
                |  connect to jira      |  asimmons     |  options= {'http://devjira01'}    | 		|
                |  ${issue}				|  create issue |  ${issue_field_dict}				|  True |
                |  add comment to issue	|  ${issue}		|  Starting work on this issue		| 		|
        """
        self.jira.add_comment(issue=issue, body=comment)

    def add_attachment_to_issue(self, issue, attachment, filename=None):
        """
        Uploads and attaches a file a specified issue. (Note: include the file extention when using the 'filename' option or this will change the file type.)

        Arguments:
            |   issue (string)  	| A JIRA Issue that a watcher needs added to, can be an issue ID or Key		|
            |   attachment (string) | A string pointing to a file location to upload and attach to the issue	|
            |   filename (string)  	| (Optional) A string to rename the file to upon attaching to the issue  	|

        Example:
            |  *Keyword*        		|  *Parameters* | 							         |						|
            |  connect to jira          |  asimmons     | options= {'http://devjira01'}      | 						|
            |  ${issue}					|  create issue |  ${issue_field_dict}	             |  True 			 	|
            |  add attchment to issue	|  ${issue}		|  ./logfile.text			         |  LogInformation.txt	|
        """
        self.jira.add_attachment(issue=issue, attachment=attachment,
                                 filename=filename)
예제 #15
0
            "Enter option for following task: \n 1. Get a story into current sprint \n 2. Move Story \n 3. Update Story Points \n 4. Read Story's Description & comments \n 5. Add work log to ticket \n 6. Assign Ticket To Me \n 7. Create sub-task of this ticket \n"
        ))

    if command == "1":
        boards = jira.boards()
        # print('boards ', boards)
        sprints = jira.sprints(board_id)
        # for sprint in sprints:
        #     print('%s: %s ' % (sprint.id, sprint.name))
        sprintId = int(sprints[-1].id)
        issueId = str(issue.id)
        issueIdList = list()
        issueIdList.append(issueId)
        # print(issue.id)
        # print("issue key ", issueIdList)
        jira.assign_issue(issue.id, username)
        jira.add_issues_to_sprint(sprintId, issueIdList)
        storyPoints = int(input("Enter story points: "))
        issue.update(fields={'customfield_10021':
                             storyPoints})  # --> Story Points
        issue.update(fields={'customfield_10904': {
            'id': "11568"
        }})  # --> Team 4 Id

        userResponse = str(input("Enter (Y/y) to continue: "))
        if userResponse.lower() != "y":
            executionFlag = False
    elif command == "2":
        transitions = jira.transitions(issue)
        # print([(t['id'], t['name']) for t in transitions])
        # print(issue.fields.status)
예제 #16
0
class Tasks(object):
    def __init__(self, configfile='bamboo.cfg'):
        self.jira_user = '******'
        self.jira_password = '******'
        self.server_name = 'https://jira.rutube.ru'
        parse_config(self, configfile)
        self.jira = JIRA({'server': self.server_name},
                         basic_auth=(self.jira_user, self.jira_password))

    def get_versions(self, task_key):
        self.issue = self.jira.issue(task_key)
        result = []
        for v in self.issue.fields.fixVersions:
            if v.archived or v.released:
                continue
            version = v.name
            if not re.match(r'^[\d]+\.[\d]+\.[\d]+$', version):
                continue
            result.append(version)
        return result

    def get_transitions(self, task_key):
        return self.jira.transitions(task_key)

    def search_tasks(self, project_key, status=None, issue_type=None,
                     assignee=None, release=None):
        query = "project = %s" % project_key
        if isinstance(status, (tuple, list)):
            statuses = ', '.join('"%s"' % s for s in status)
            query += ' AND status IN (%s)' % statuses
        if isinstance(status, str):
            query += ' AND status = "%s"' % status
        if isinstance(issue_type, (tuple, list)):
            types = ', '.join('"%s"' % t for t in issue_type)
            query += ' AND type IN (%s)' % types
        if isinstance(issue_type, str):
            query += ' AND type = "%s"' % issue_type
        if assignee:
            if assignee == 'currentUser()':
                query += ' AND assignee=currentUser()'
            else:
                query += ' AND assignee="%s"' % assignee
        if release:
            query += ' AND fixVersion="%s"' % release
        return self.jira.search_issues(query)

    def transition(self, task_key, transition_id):
        self.jira.transition_issue(task_key, transition_id)

    def assign(self, task_key, assignee):
        self.jira.assign_issue(task_key, assignee)

    def get_assignee(self, task_key):
        issue = self.jira.issue(task_key)
        return issue.fields.assignee.name

    def task_info(self, task_key):
        issue = self.jira.issue(task_key)
        result = (
            ('key', issue.key),
            ('title', issue.fields.summary),
            ('assignee', issue.fields.assignee.name),
            ('status', issue.fields.status.name),
        )
        return result

    def move(self, task_key, transition_name):
        transition_name = transition_name.lower().replace(' ', '-')
        transitions = self.get_transitions(task_key)
        for trans in transitions:
            name = trans['to']['name'].lower().replace(' ', '-')
            if name == transition_name:
                self.transition(task_key, trans['id'])
                return True
        return False
예제 #17
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'
        # 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
예제 #18
0
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)