from typing import Optional
import bi_etl.config.notifiers_config as notifiers_config
from bi_etl.notifiers.notifier_base import NotifierBase
[docs]
class Jira(NotifierBase):
[docs]
def __init__(self, config_section: notifiers_config.JiraNotifier, *, name: Optional[str] = None):
super().__init__(name=name)
self.config_section = config_section
# On-instance import since jira is an optional requirement
# noinspection PyUnresolvedReferences
from jira.client import JIRA
# noinspection PyUnresolvedReferences
from jira.exceptions import JIRAError
self.config_section = config_section
options = dict()
options['server'] = self.config_section.server
user_id = self.config_section.user_id
self.project = self.config_section.project
password = self.config_section.get_password()
self.log.debug(f"user id={user_id}")
self.log.debug(f"server={options['server']}")
self.log.debug(f'project={self.project}')
try:
self.jira_conn = JIRA(options, basic_auth=(user_id, password))
except JIRAError as e:
if 'CAPTCHA_CHALLENGE' in e.text:
raise RuntimeError(f'Jira Login requests passing CAPTCHA CHALLENGE. {e.text}')
else:
self.log.error(f'Error connecting to JIRA')
self.log.exception(e)
raise
priority_name = self.config_section.priority
if priority_name is not None:
for priority_object in self.jira_conn.priorities():
if priority_object.name == priority_name:
self.priority_id = priority_object.id
self.log.debug(f'priority_name = {priority_name} priority_id={self.priority_id}')
else:
self.priority_id = None
self.log.debug('priority not specified in config')
self.subject_prefix = self.config_section.subject_prefix
self.comment_on_each_instance = self.config_section.comment_on_each_instance
self.component = self.config_section.component
self.issue_type = self.config_section.issue_type
exclude_statuses = self.config_section.exclude_statuses
exclude_statuses_filter_list = []
for status in exclude_statuses:
exclude_statuses_filter_list.append(f'"{status}"')
self.exclude_statuses_filter = ','.join(exclude_statuses_filter_list)
[docs]
def add_attachment(self, issue, attachment):
"""Attach an attachment to an issue and returns a Resource for it.
The client will *not* attempt to open or validate the attachment; it expects a file-like object to be ready
for its use. The user is still responsible for tidying up (e.g., closing the file, killing the socket, etc.)
:param issue: the issue to attach the attachment to
:param attachment:
file-like object to attach to the issue, also works if it is a string with the filename,
or a tuple with a file-like object and a filename.
If the (file, filename) tuple is not used the file object's ``name`` attribute
is used. If you acquired the file-like object by any other method than ``open()``, make sure
that a name is specified in one way or the other.
:rtype: an Attachment Resource
"""
if isinstance(attachment, tuple):
attachment, filename = attachment
else:
filename = None
return self.jira_conn.add_attachment(issue=issue, attachment=attachment, filename=filename)
[docs]
def search(self, subject):
# Find already opened case, if there is one
found_issues = list()
# Remove any special characters that break JQL parsing
# https://support.atlassian.com/jira-software-cloud/docs/search-syntax-for-text-fields/
subject_escaped = subject
reserved_list = [
'\\', '+', '-', '[', ']', '(', ')', '{', '}',
'AND', 'OR', 'NOT',
'"', "'", '|', '&&', '!', '*', ':',
'?', '~', '^', '%',
'\t', '\n', '\r',
]
for reserved in reserved_list:
subject_escaped = subject_escaped.replace(reserved, ' ')
issues = self.jira_conn.search_issues(
f'project="{self.project}" '
f'AND summary~"{subject_escaped}" '
f'AND status not in ({self.exclude_statuses_filter})'
)
for iss in issues:
# Double check that name matches since JIRA does a wildcard search and word stemming
if iss.fields.summary.strip() == subject:
# self.log.debug('Potential match:')
# self.log.debug(p.fields.status)
# self.log.debug(p.fields.summary)
# self.log.debug(p.fields.description)
found_issues.append(iss)
case_number = iss.key
return found_issues
[docs]
def send(self, subject, message, sensitive_message=None, attachment=None, throw_exception=False):
"""
Log a Jira issue
To use special formatting codes plesae see
https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa?section=all
:param subject:
:param message:
:param sensitive_message:
:param attachment:
:param throw_exception:
:return:
"""
if subject is None:
raise ValueError(f"Jira notifier requires a valid subject. Message was {message}")
else:
subject = self.subject_prefix + subject.strip()
self.log.debug(f'subject={subject}')
self.log.debug(f'message={message}')
if message is None:
message = "_No Description Provided_"
message_parts = [
message,
]
if sensitive_message is not None:
message_parts.append(sensitive_message)
existing_issues = self.search(subject)
if len(existing_issues) > 1:
self.log.warning(f"Found multiple open issues with subject {subject}. Finding newest...")
newest_case_number = 0
newest_iss = None
for iss in existing_issues:
case_number = iss.key
self.log.info(f"One of multiple existing open cases is {case_number}.")
# Fixed the issue in the file by getting the int value for case_number
proj_code, case_num = case_number.split('-')
case_num_int = int(case_num)
if case_num_int > newest_case_number:
newest_case_number = case_num_int
newest_iss = iss
existing_issues = [newest_iss]
# Allow the section below to comment on the newest issue
if len(existing_issues) == 1:
iss = existing_issues[0]
case_number = iss.key
self.log.info(f"Found existing open case {case_number}.")
if self.comment_on_each_instance:
if attachment is not None:
attachment_object = self.add_attachment(iss, attachment)
self.log.debug(f"Created attachment {attachment_object}")
message_parts.insert(0, "New occurrence with message(s):")
if message or sensitive_message:
comment = '\n'.join(message_parts)
self.jira_conn.add_comment(iss, comment)
self.log.info(f"Added comment to case {case_number}.")
else:
description = '\n'.join(message_parts)
issue_dict = {
'project': {'key': self.project},
'summary': subject,
'description': description,
}
if self.issue_type is not None:
issue_dict['issuetype'] = {'name': self.issue_type}
if self.priority_id:
issue_dict['priority'] = {'id': self.priority_id}
if self.component:
issue_dict['components'] = [{'name': self.component}, ]
self.log.debug(f'issue_dict={issue_dict}')
new_issue = self.jira_conn.create_issue(fields=issue_dict)
case_number = new_issue.key
self.log.info(f"Created new case {case_number}")
if attachment is not None:
attachment_object = self.add_attachment(new_issue, attachment)
self.log.debug(f"Created attachment {attachment_object}")