Notification Engine for Monasca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

jira_notifier.py 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. # (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP
  2. # Copyright 2017 Fujitsu LIMITED
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  13. # implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. from jinja2 import Template
  17. import jira
  18. from six.moves import urllib
  19. import ujson as json
  20. import yaml
  21. from debtcollector import removals
  22. from oslo_config import cfg
  23. from monasca_notification.plugins.abstract_notifier import AbstractNotifier
  24. """
  25. Note:
  26. This plugin doesn't support multi tenancy. Multi tenancy requires support for
  27. multiple JIRA server url. JIRA doesn't support OAUTH2 tokens, we may need to get
  28. the user credentials in query params and store them in monasca DB which we don't want to do.
  29. That is the reason for not supporting true multitenancy.
  30. MultiTenancy can be achieved by creating issues in different project for different tenant on
  31. the same JIRA server.
  32. notification.address = https://<jira_url>/?project=<project_name>
  33. Dependency for Jira
  34. 1) Jira plugin requires Jira library. Consumers need to install
  35. JIRA via pip
  36. 2) (i.e) pip install jira
  37. Jira Configuration
  38. 1) jira:
  39. user: username
  40. password: password
  41. Sample notification:
  42. monasca notification-create MyIssuer JIRA https://jira.hpcloud.net/?project=MyProject
  43. monasca notification-create MyIssuer1 JIRA https://jira.hpcloud.net/?project=MyProject&
  44. component=MyComponent
  45. """
  46. CONF = cfg.CONF
  47. def register_opts(conf):
  48. gr = cfg.OptGroup(name='%s_notifier' % JiraNotifier.type)
  49. opts = [
  50. cfg.IntOpt(name='timeout', default=5, min=1),
  51. cfg.StrOpt(name='user', required=False),
  52. cfg.StrOpt(name='password', required=False, secret=True),
  53. cfg.StrOpt(name='custom_formatter', default=None),
  54. cfg.StrOpt(name='proxy', default=None)
  55. ]
  56. conf.register_group(gr)
  57. conf.register_opts(opts, group=gr)
  58. class JiraNotifier(AbstractNotifier):
  59. type = 'jira'
  60. _search_query = search_query = "project={} and reporter='{}' and summary ~ '{}'"
  61. def __init__(self, log):
  62. super(JiraNotifier, self).__init__()
  63. self._log = log
  64. self.jira_fields_format = None
  65. @removals.remove(
  66. message='Configuration of notifier is available through oslo.cfg',
  67. version='1.9.0',
  68. removal_version='3.0.0'
  69. )
  70. def config(self, config_dict):
  71. pass
  72. @property
  73. def statsd_name(self):
  74. return 'jira_notifier'
  75. def _get_jira_custom_format_fields(self):
  76. jira_fields_format = None
  77. formatter = CONF.jira_notifier.custom_formatter
  78. if not self.jira_fields_format and formatter:
  79. try:
  80. with open(formatter, 'r') as f:
  81. jira_fields_format = yaml.safe_load(f)
  82. except Exception:
  83. self._log.exception("Unable to read custom_formatter file. Check file location")
  84. raise
  85. # Remove the top element
  86. jira_fields_format = jira_fields_format["jira_format"]
  87. return jira_fields_format
  88. def _build_custom_jira_message(self, notification, jira_fields_format):
  89. jira_fields = {}
  90. # Templatize the message object
  91. jira_field_summary_field = jira_fields_format.get("summary", None)
  92. if jira_field_summary_field:
  93. template = Template(jira_field_summary_field)
  94. jira_fields["summary"] = template.render(notification=notification)
  95. jira_field_comments_field = jira_fields_format.get("comments", None)
  96. if jira_field_comments_field:
  97. template = Template(jira_field_comments_field)
  98. jira_fields["comments"] = template.render(notification=notification)
  99. jira_field_description_field = jira_fields_format.get("description", None)
  100. if jira_field_description_field:
  101. template = Template(jira_field_description_field)
  102. jira_fields["description"] = template.render(notification=notification)
  103. return jira_fields
  104. def _build_default_jira_message(self, notification):
  105. """Builds jira message body
  106. """
  107. body = {'alarm_id': notification.alarm_id,
  108. 'alarm_definition_id': notification.raw_alarm['alarmDefinitionId'],
  109. 'alarm_name': notification.alarm_name,
  110. 'alarm_description': notification.raw_alarm['alarmDescription'],
  111. 'alarm_timestamp': notification.alarm_timestamp,
  112. 'state': notification.state,
  113. 'old_state': notification.raw_alarm['oldState'],
  114. 'message': notification.message,
  115. 'tenant_id': notification.tenant_id,
  116. 'metrics': notification.metrics}
  117. jira_fields = {}
  118. summary_format_string = ("Monasca alarm for alarm_defintion {0} status changed to {1} "
  119. "for the alarm_id {2}")
  120. jira_fields["summary"] = summary_format_string.format(notification.alarm_name,
  121. notification.state,
  122. notification.alarm_id)
  123. jira_fields["comments"] = "{code}%s{code}" % (json.dumps(body, indent=3))
  124. jira_fields["description"] = 'Monasca alarm'
  125. return jira_fields
  126. def _build_jira_message(self, notification):
  127. formatter = CONF.jira_notifier.custom_formatter
  128. if formatter:
  129. return self._build_custom_jira_message(notification,
  130. self._get_jira_custom_format_fields())
  131. return self._build_default_jira_message(notification)
  132. def send_notification(self, notification):
  133. """Creates or Updates an issue in Jira
  134. """
  135. jira_fields = self._build_jira_message(notification)
  136. parsed_url = urllib.parse.urlsplit(notification.address)
  137. query_params = urllib.parse.parse_qs(parsed_url.query)
  138. # URL without query params
  139. url = urllib.parse.urljoin(
  140. notification.address,
  141. urllib.parse.urlparse(
  142. notification.address).path)
  143. jira_fields["project"] = query_params["project"][0]
  144. if query_params.get("component"):
  145. jira_fields["component"] = query_params["component"][0]
  146. auth = (
  147. CONF.jira_notifier.user,
  148. CONF.jira_notifier.password
  149. )
  150. proxy = CONF.jira_notifier.proxy
  151. proxy_dict = None
  152. if proxy is not None:
  153. proxy_dict = {"https": proxy}
  154. try:
  155. jira_obj = jira.JIRA(url, basic_auth=auth, proxies=proxy_dict)
  156. self.jira_workflow(jira_fields, jira_obj, notification)
  157. except Exception:
  158. self._log.exception("Error creating issue in Jira at URL {}".format(url))
  159. return False
  160. return True
  161. def jira_workflow(self, jira_fields, jira_obj, notification):
  162. """How does Jira plugin work?
  163. 1) Check whether the issue with same description exists?
  164. 2) If issue exists, and if it is closed state, open it
  165. 3) if the issue doesn't exist, then create the issue
  166. 4) Add current alarm details in comments
  167. """
  168. issue_dict = {'project': {'key': jira_fields["project"]},
  169. 'summary': jira_fields["summary"],
  170. 'description': jira_fields["description"],
  171. 'issuetype': {'name': 'Bug'}, }
  172. # If the JIRA workflow is created with mandatory components
  173. if jira_fields.get("component"):
  174. issue_dict["components"] = [{"name": jira_fields.get("component")}]
  175. search_term = self._search_query.format(issue_dict["project"]["key"],
  176. CONF.jira_notifier.user,
  177. notification.alarm_id)
  178. issue_list = jira_obj.search_issues(search_term)
  179. if not issue_list:
  180. self._log.debug("Creating an issue with the data {}".format(issue_dict))
  181. issue = jira_obj.create_issue(fields=issue_dict)
  182. else:
  183. issue = issue_list[0]
  184. self._log.debug("Found an existing issue {} for this notification".format(issue))
  185. current_state = issue.fields.status.name
  186. if current_state.lower() in ["resolved", "closed"]:
  187. # Open the the issue
  188. transitions = jira_obj.transitions(issue)
  189. allowed_transistions = [(t['id'], t['name'])
  190. for t in transitions if "reopen" in t['name'].lower()]
  191. if allowed_transistions:
  192. # Reopen the issue
  193. jira_obj.transition_issue(issue, allowed_transistions[0][0])
  194. jira_comment_message = jira_fields.get("comments")
  195. if jira_comment_message:
  196. jira_obj.add_comment(issue, jira_comment_message)