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.

email_notifier.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. # (C) Copyright 2015-2016 Hewlett Packard Enterprise Development 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. import email.header
  17. import email.mime.text
  18. import email.utils
  19. import six
  20. import smtplib
  21. import time
  22. from debtcollector import removals
  23. from oslo_config import cfg
  24. from monasca_notification.plugins import abstract_notifier
  25. CONF = cfg.CONF
  26. EMAIL_SINGLE_HOST_BASE = u'''On host "{hostname}" for target "{target_host}" {message}
  27. Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC
  28. alarm_id: {alarm_id}
  29. Lifecycle state: {lifecycle_state}
  30. Link: {link}
  31. Link to Grafana: {grafana_url}
  32. With dimensions:
  33. {metric_dimensions}'''
  34. EMAIL_MULTIPLE_HOST_BASE = u'''On host "{hostname}" {message}
  35. Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC
  36. alarm_id: {alarm_id}
  37. Lifecycle state: {lifecycle_state}
  38. Link: {link}
  39. Link to Grafana: {grafana_url}
  40. With dimensions:
  41. {metric_dimensions}'''
  42. EMAIL_NO_HOST_BASE = u'''On multiple hosts {message}
  43. Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC
  44. Alarm_id: {alarm_id}
  45. Lifecycle state: {lifecycle_state}
  46. Link: {link}
  47. Link to Grafana: {grafana_url}
  48. With dimensions
  49. {metric_dimensions}'''
  50. def register_opts(conf):
  51. gr = cfg.OptGroup(name='%s_notifier' % EmailNotifier.type)
  52. opts = [
  53. cfg.StrOpt(name='from_addr'),
  54. cfg.HostAddressOpt(name='server'),
  55. cfg.PortOpt(name='port', default=25),
  56. cfg.IntOpt(name='timeout', default=5, min=1),
  57. cfg.StrOpt(name='user', default=None),
  58. cfg.StrOpt(name='password', default=None, secret=True),
  59. cfg.StrOpt(name='grafana_url', default=None)
  60. ]
  61. conf.register_group(gr)
  62. conf.register_opts(opts, group=gr)
  63. class EmailNotifier(abstract_notifier.AbstractNotifier):
  64. type = 'email'
  65. def __init__(self, log):
  66. super(EmailNotifier, self).__init__()
  67. self._log = log
  68. self._smtp = None
  69. @removals.remove(
  70. message='Configuration of notifier is available through oslo.cfg',
  71. version='1.9.0',
  72. removal_version='3.0.0'
  73. )
  74. def config(self, config=None):
  75. self._smtp_connect()
  76. @property
  77. def statsd_name(self):
  78. return "sent_smtp_count"
  79. def send_notification(self, notification):
  80. """Send the notification via email
  81. Returns the True upon success, False upon failure
  82. """
  83. # Get the "hostname" from the notification metrics if there is one
  84. hostname = []
  85. targethost = []
  86. for metric in notification.metrics:
  87. dimap = metric['dimensions']
  88. if 'hostname' in dimap and not dimap['hostname'] in hostname:
  89. hostname.append(dimap['hostname'])
  90. if 'target_host' in dimap and not dimap['target_host'] in targethost:
  91. targethost.append(dimap['target_host'])
  92. # Generate the message
  93. msg = self._create_msg(hostname, notification, targethost)
  94. if not self._smtp and not self._smtp_connect():
  95. return False
  96. try:
  97. self._sendmail(notification, msg)
  98. return True
  99. except smtplib.SMTPServerDisconnected:
  100. self._log.warn('SMTP server disconnected. '
  101. 'Will reconnect and retry message.')
  102. self._smtp_connect()
  103. except smtplib.SMTPException:
  104. self._email_error(notification)
  105. return False
  106. try:
  107. self._sendmail(notification, msg)
  108. return True
  109. except smtplib.SMTPException:
  110. self._email_error(notification)
  111. return False
  112. def _sendmail(self, notification, msg):
  113. self._smtp.sendmail(CONF.email_notifier.from_addr,
  114. notification.address,
  115. msg.as_string())
  116. self._log.debug("Sent email to {}, notification {}".format(notification.address,
  117. notification.to_json()))
  118. def _email_error(self, notification):
  119. self._log.exception("Error sending Email Notification")
  120. self._log.error("Failed email: {}".format(notification.to_json()))
  121. def _smtp_connect(self):
  122. """Connect to the smtp server
  123. """
  124. self._log.info("Connecting to Email Server {}".format(
  125. CONF.email_notifier.server))
  126. try:
  127. smtp = smtplib.SMTP(CONF.email_notifier.server,
  128. CONF.email_notifier.port,
  129. timeout=CONF.email_notifier.timeout)
  130. email_notifier_user = CONF.email_notifier.user
  131. email_notifier_password = CONF.email_notifier.password
  132. if email_notifier_user and email_notifier_password:
  133. smtp.login(email_notifier_user,
  134. email_notifier_password)
  135. self._smtp = smtp
  136. return True
  137. except Exception:
  138. self._log.exception("Unable to connect to email server.")
  139. return False
  140. def _create_msg(self, hostname, notification, targethost=None):
  141. """Create two kind of messages:
  142. 1. Notifications that include metrics with a hostname as a dimension. There may be more than one hostname.
  143. We will only report the hostname if there is only one.
  144. 2. Notifications that do not include metrics and therefore no hostname. Example: API initiated changes.
  145. * A third notification type which include metrics but do not include a hostname will
  146. be treated as type #2.
  147. """
  148. timestamp = time.asctime(time.gmtime(notification.alarm_timestamp))
  149. alarm_seconds = notification.alarm_timestamp
  150. alarm_ms = int(round(alarm_seconds * 1000))
  151. graf_url = self._get_link_url(notification.metrics[0], alarm_ms)
  152. dimensions = _format_dimensions(notification)
  153. if len(hostname) == 1: # Type 1
  154. if targethost:
  155. text = EMAIL_SINGLE_HOST_BASE.format(
  156. hostname=hostname[0],
  157. target_host=targethost[0],
  158. message=notification.message.lower(),
  159. alarm_name=notification.alarm_name,
  160. state=notification.state,
  161. timestamp=timestamp,
  162. alarm_id=notification.alarm_id,
  163. metric_dimensions=dimensions,
  164. link=notification.link,
  165. grafana_url=graf_url,
  166. lifecycle_state=notification.lifecycle_state
  167. )
  168. subject = u'{} {} "{}" for Host: {} Target: {}'.format(
  169. notification.state, notification.severity,
  170. notification.alarm_name, hostname[0],
  171. targethost[0]
  172. )
  173. else:
  174. text = EMAIL_MULTIPLE_HOST_BASE.format(
  175. hostname=hostname[0],
  176. message=notification.message.lower(),
  177. alarm_name=notification.alarm_name,
  178. state=notification.state,
  179. timestamp=timestamp,
  180. alarm_id=notification.alarm_id,
  181. metric_dimensions=dimensions,
  182. link=notification.link,
  183. grafana_url=graf_url,
  184. lifecycle_state=notification.lifecycle_state
  185. )
  186. subject = u'{} {} "{}" for Host: {}'.format(
  187. notification.state, notification.severity,
  188. notification.alarm_name, hostname[0])
  189. else: # Type 2
  190. text = EMAIL_NO_HOST_BASE.format(
  191. message=notification.message.lower(),
  192. alarm_name=notification.alarm_name,
  193. state=notification.state,
  194. timestamp=timestamp,
  195. alarm_id=notification.alarm_id,
  196. metric_dimensions=dimensions,
  197. link=notification.link,
  198. grafana_url=graf_url,
  199. lifecycle_state=notification.lifecycle_state
  200. )
  201. subject = u'{} {} "{}" '.format(notification.state,
  202. notification.severity,
  203. notification.alarm_name)
  204. msg = email.mime.text.MIMEText(text, 'plain', 'utf-8')
  205. msg['Subject'] = email.header.Header(subject, 'utf-8')
  206. msg['From'] = CONF.email_notifier.from_addr
  207. msg['To'] = notification.address
  208. msg['Date'] = email.utils.formatdate(localtime=True, usegmt=True)
  209. return msg
  210. def _get_link_url(self, metric, timestamp_ms):
  211. """Returns the url to Grafana including a query with the
  212. respective metric info (name, dimensions, timestamp)
  213. :param metric: the metric for which to display the graph in Grafana
  214. :param timestamp_ms: timestamp of the alarm for the metric in milliseconds
  215. :return: the url to the graph for the given metric or None if no Grafana host
  216. has been defined.
  217. """
  218. grafana_url = CONF.email_notifier.grafana_url
  219. if grafana_url is None:
  220. return None
  221. url = ''
  222. metric_query = ''
  223. metric_query = "?metric=%s" % metric['name']
  224. dimensions = metric['dimensions']
  225. for key, value in six.iteritems(dimensions):
  226. metric_query += "&dim_%s=%s" % (key, value)
  227. # Show the graph within a range of ten minutes before and after the alarm occurred.
  228. offset = 600000
  229. from_ms = timestamp_ms - offset
  230. to_ms = timestamp_ms + offset
  231. time_query = "&from=%s&to=%s" % (from_ms, to_ms)
  232. url = grafana_url + '/dashboard/script/drilldown.js'
  233. return url + metric_query + time_query
  234. def _format_dimensions(notification):
  235. dimension_sets = []
  236. for metric in notification.metrics:
  237. dimension_sets.append(metric['dimensions'])
  238. dim_set_strings = []
  239. for dimension_set in dimension_sets:
  240. key_value_pairs = []
  241. for key, value in dimension_set.items():
  242. key_value_pairs.append(u' {}: {}'.format(key, value))
  243. set_string = u' {\n' + u',\n'.join(key_value_pairs) + u'\n }'
  244. dim_set_strings.append(set_string)
  245. dimensions = u'[\n' + u',\n'.join(dim_set_strings) + u' \n]'
  246. return dimensions