web_alert.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. #!/usr/bin/env python
  2. """
  3. Licensed to the Apache Software Foundation (ASF) under one
  4. or more contributor license agreements. See the NOTICE file
  5. distributed with this work for additional information
  6. regarding copyright ownership. The ASF licenses this file
  7. to you under the Apache License, Version 2.0 (the
  8. "License"); you may not use this file except in compliance
  9. with the License. You may obtain a copy of the License at
  10. http://www.apache.org/licenses/LICENSE-2.0
  11. Unless required by applicable law or agreed to in writing, software
  12. distributed under the License is distributed on an "AS IS" BASIS,
  13. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. See the License for the specific language governing permissions and
  15. limitations under the License.
  16. """
  17. import logging
  18. import time
  19. import subprocess
  20. import os
  21. import urllib2
  22. from urllib2 import HTTPError
  23. import uuid
  24. from tempfile import gettempdir
  25. from alerts.base_alert import BaseAlert
  26. from collections import namedtuple
  27. from resource_management.libraries.functions.get_port_from_url import get_port_from_url
  28. from resource_management.libraries.functions import get_kinit_path
  29. from resource_management.libraries.functions import get_klist_path
  30. from ambari_commons import OSCheck
  31. from ambari_commons.inet_utils import resolve_address
  32. # hashlib is supplied as of Python 2.5 as the replacement interface for md5
  33. # and other secure hashes. In 2.6, md5 is deprecated. Import hashlib if
  34. # available, avoiding a deprecation warning under 2.6. Import md5 otherwise,
  35. # preserving 2.4 compatibility.
  36. try:
  37. import hashlib
  38. _md5 = hashlib.md5
  39. except ImportError:
  40. import md5
  41. _md5 = md5.new
  42. logger = logging.getLogger()
  43. # default timeout
  44. DEFAULT_CONNECTION_TIMEOUT = 5
  45. WebResponse = namedtuple('WebResponse', 'status_code time_millis error_msg')
  46. class WebAlert(BaseAlert):
  47. def __init__(self, alert_meta, alert_source_meta, config):
  48. super(WebAlert, self).__init__(alert_meta, alert_source_meta)
  49. connection_timeout = DEFAULT_CONNECTION_TIMEOUT
  50. # extract any lookup keys from the URI structure
  51. self.uri_property_keys = None
  52. if 'uri' in alert_source_meta:
  53. uri = alert_source_meta['uri']
  54. self.uri_property_keys = self._lookup_uri_property_keys(uri)
  55. if 'connection_timeout' in uri:
  56. connection_timeout = uri['connection_timeout']
  57. # python uses 5.0, CURL uses "5"
  58. self.connection_timeout = float(connection_timeout)
  59. self.curl_connection_timeout = str(int(connection_timeout))
  60. self.config = config
  61. def _collect(self):
  62. if self.uri_property_keys is None:
  63. raise Exception("Could not determine result. URL(s) were not defined.")
  64. # use the URI lookup keys to get a final URI value to query
  65. alert_uri = self._get_uri_from_structure(self.uri_property_keys)
  66. logger.debug("[Alert][{0}] Calculated web URI to be {1} (ssl={2})".format(
  67. self.get_name(), alert_uri.uri, str(alert_uri.is_ssl_enabled)))
  68. url = self._build_web_query(alert_uri)
  69. # substitute 0.0.0.0 in url with actual fqdn
  70. url = url.replace('0.0.0.0', self.host_name)
  71. web_response = self._make_web_request(url)
  72. status_code = web_response.status_code
  73. time_seconds = web_response.time_millis / 1000
  74. error_message = web_response.error_msg
  75. if status_code == 0:
  76. return (self.RESULT_CRITICAL, [status_code, url, time_seconds, error_message])
  77. # anything that's less than 400 is OK
  78. if status_code < 400:
  79. return (self.RESULT_OK, [status_code, url, time_seconds])
  80. # everything else is WARNING
  81. return (self.RESULT_WARNING, [status_code, url, time_seconds, error_message])
  82. def _build_web_query(self, alert_uri):
  83. """
  84. Builds a URL out of the URI structure. If the URI is already a URL of
  85. the form http[s]:// then this will return the URI as the URL; otherwise,
  86. it will build the URL from the URI structure's elements
  87. """
  88. # shortcut if the supplied URI starts with the information needed
  89. string_uri = str(alert_uri.uri)
  90. if string_uri.startswith('http://') or string_uri.startswith('https://'):
  91. return alert_uri.uri
  92. # start building the URL manually
  93. host = BaseAlert.get_host_from_url(alert_uri.uri)
  94. if host is None:
  95. host = self.host_name
  96. # maybe slightly realistic
  97. port = 80
  98. if alert_uri.is_ssl_enabled is True:
  99. port = 443
  100. # extract the port
  101. try:
  102. port = int(get_port_from_url(alert_uri.uri))
  103. except:
  104. pass
  105. scheme = 'http'
  106. if alert_uri.is_ssl_enabled is True:
  107. scheme = 'https'
  108. if OSCheck.is_windows_family():
  109. # on windows 0.0.0.0 is invalid address to connect but on linux it resolved to 127.0.0.1
  110. host = resolve_address(host)
  111. return "{0}://{1}:{2}".format(scheme, host, str(port))
  112. def _make_web_request(self, url):
  113. """
  114. Makes an http(s) request to a web resource and returns the http code. If
  115. there was an error making the request, return 0 for the status code.
  116. """
  117. error_msg = None
  118. try:
  119. response_code = 0
  120. kerberos_keytab = None
  121. kerberos_principal = None
  122. if self.uri_property_keys.kerberos_principal is not None:
  123. kerberos_principal = self._get_configuration_value(
  124. self.uri_property_keys.kerberos_principal)
  125. if kerberos_principal is not None:
  126. # substitute _HOST in kerberos principal with actual fqdn
  127. kerberos_principal = kerberos_principal.replace('_HOST', self.host_name)
  128. if self.uri_property_keys.kerberos_keytab is not None:
  129. kerberos_keytab = self._get_configuration_value(self.uri_property_keys.kerberos_keytab)
  130. if kerberos_principal is not None and kerberos_keytab is not None:
  131. # Create the kerberos credentials cache (ccache) file and set it in the environment to use
  132. # when executing curl. Use the md5 hash of the combination of the principal and keytab file
  133. # to generate a (relatively) unique cache filename so that we can use it as needed.
  134. tmp_dir = self.config.get('agent', 'tmp_dir')
  135. if tmp_dir is None:
  136. tmp_dir = gettempdir()
  137. ccache_file_name = _md5("{0}|{1}".format(kerberos_principal, kerberos_keytab)).hexdigest()
  138. ccache_file_path = "{0}{1}web_alert_cc_{2}".format(tmp_dir, os.sep, ccache_file_name)
  139. kerberos_env = {'KRB5CCNAME': ccache_file_path}
  140. # Get the configured Kerberos executables search paths, if any
  141. kerberos_executable_search_paths = self._get_configuration_value('{{kerberos-env/executable_search_paths}}')
  142. # If there are no tickets in the cache or they are expired, perform a kinit, else use what
  143. # is in the cache
  144. klist_path_local = get_klist_path(kerberos_executable_search_paths)
  145. if os.system("{0} -s {1}".format(klist_path_local, ccache_file_path)) != 0:
  146. kinit_path_local = get_kinit_path(kerberos_executable_search_paths)
  147. logger.debug("[Alert][{0}] Enabling Kerberos authentication via GSSAPI using ccache at {1}.".format(
  148. self.get_name(), ccache_file_path))
  149. os.system("{0} -l 5m -c {1} -kt {2} {3} > /dev/null".format(
  150. kinit_path_local, ccache_file_path, kerberos_keytab,
  151. kerberos_principal))
  152. else:
  153. logger.debug("[Alert][{0}] Kerberos authentication via GSSAPI already enabled using ccache at {1}.".format(
  154. self.get_name(), ccache_file_path))
  155. # check if cookies dir exists, if not then create it
  156. tmp_dir = self.config.get('agent', 'tmp_dir')
  157. cookies_dir = os.path.join(tmp_dir, "cookies")
  158. if not os.path.exists(cookies_dir):
  159. os.makedirs(cookies_dir)
  160. cookie_file_name = str(uuid.uuid4())
  161. cookie_file = os.path.join(cookies_dir, cookie_file_name)
  162. start_time = time.time()
  163. try:
  164. curl = subprocess.Popen(['curl', '--negotiate', '-u', ':', '-b', cookie_file, '-c', cookie_file, '-sL', '-w',
  165. '%{http_code}', url, '--connect-timeout', self.curl_connection_timeout,
  166. '-o', '/dev/null'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=kerberos_env)
  167. curl_stdout, curl_stderr = curl.communicate()
  168. finally:
  169. if os.path.isfile(cookie_file):
  170. os.remove(cookie_file)
  171. # empty quotes evaluates to false
  172. if curl_stderr:
  173. error_msg = curl_stderr
  174. # empty quotes evaluates to false
  175. if curl_stdout:
  176. response_code = int(curl_stdout)
  177. time_millis = time.time() - start_time
  178. else:
  179. # kerberos is not involved; use urllib2
  180. response_code, time_millis, error_msg = self._make_web_request_urllib(url)
  181. return WebResponse(status_code=response_code, time_millis=time_millis,
  182. error_msg=error_msg)
  183. except Exception, exception:
  184. if logger.isEnabledFor(logging.DEBUG):
  185. logger.exception("[Alert][{0}] Unable to make a web request.".format(self.get_name()))
  186. return WebResponse(status_code=0, time_millis=0, error_msg=str(exception))
  187. def _make_web_request_urllib(self, url):
  188. """
  189. Make a web request using urllib2. This function does not handle exceptions.
  190. :param url: the URL to request
  191. :return: a tuple of the response code and the total time in ms
  192. """
  193. response = None
  194. error_message = None
  195. start_time = time.time()
  196. try:
  197. response = urllib2.urlopen(url, timeout=self.connection_timeout)
  198. response_code = response.getcode()
  199. time_millis = time.time() - start_time
  200. return response_code, time_millis, error_message
  201. except HTTPError, httpError:
  202. time_millis = time.time() - start_time
  203. error_message = str(httpError)
  204. return httpError.code, time_millis, error_message
  205. finally:
  206. if response is not None:
  207. try:
  208. response.close()
  209. except Exception, exception:
  210. if logger.isEnabledFor(logging.DEBUG):
  211. logger.exception("[Alert][{0}] Unable to close socket connection".format(self.get_name()))
  212. def _get_reporting_text(self, state):
  213. '''
  214. Gets the default reporting text to use when the alert definition does not
  215. contain any.
  216. :param state: the state of the alert in uppercase (such as OK, WARNING, etc)
  217. :return: the parameterized text
  218. '''
  219. if state == self.RESULT_CRITICAL:
  220. return 'Connection failed to {1}'
  221. return 'HTTP {0} response in {2:.4f} seconds'