#!/usr/bin/env python

"""
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements.  See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.  The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

import logging
import re
import time
from collections import namedtuple

logger = logging.getLogger()

# create a named tuple to return both the concrete URI and SSL flag
AlertUri = namedtuple('AlertUri', 'uri is_ssl_enabled')

class BaseAlert(object):
  RESULT_OK = "OK"
  RESULT_WARNING = "WARNING"
  RESULT_CRITICAL = "CRITICAL"
  RESULT_UNKNOWN = "UNKNOWN"
  RESULT_SKIPPED = "SKIPPED"

  HA_NAMESERVICE_PARAM = "{{ha-nameservice}}"
  HA_ALIAS_PARAM = "{{alias}}"

  def __init__(self, alert_meta, alert_source_meta):
    self.alert_meta = alert_meta
    self.alert_source_meta = alert_source_meta
    self.cluster_name = ''
    self.host_name = ''
    
    
  def interval(self):
    """ gets the defined interval this check should run """
    if not self.alert_meta.has_key('interval'):
      return 1
    else:
      interval = self.alert_meta['interval']
      return 1 if interval < 1 else interval


  def is_enabled(self):
    """
    gets whether the definition is enabled
    """
    return self.alert_meta['enabled']
  

  def get_name(self):
    """
    gets the unique name of the alert definition
    """
    return self.alert_meta['name']


  def get_uuid(self):
    """
    gets the unique has of the alert definition
    """
    return self.alert_meta['uuid']


  def set_helpers(self, collector, cluster_configuration):
    """
    sets helper objects for alerts without having to use them in a constructor
    """
    self.collector = collector
    self.cluster_configuration = cluster_configuration


  def set_cluster(self, cluster_name, host_name):
    """ sets cluster information for the alert """
    self.cluster_name = cluster_name
    self.host_name = host_name


  def _get_alert_meta_value_safely(self, meta_key):
    """
    safe way to get a value when outputting result json.  will not throw an exception
    """
    if self.alert_meta.has_key(meta_key):
      return self.alert_meta[meta_key]
    else:
      return None


  def collect(self):
    """ method used for collection.  defers to _collect() """
    
    res = (BaseAlert.RESULT_UNKNOWN, [])
    res_base_text = None
    
    try:
      res = self._collect()
      result_state = res[0]
      reporting_state = result_state.lower()

      # if the alert reports that it should be SKIPPED, then skip it
      # this is useful for cases where the alert might run on multiple hosts
      # but only 1 host should report the data
      if result_state == BaseAlert.RESULT_SKIPPED:
        logger.debug('[Alert][{0}] Skipping UUID {1}.'.format(self.get_name(),
          self.get_uuid()))

        return

      # it's possible that the alert definition doesn't have reporting; safely
      # check for it and fallback to default text if it doesn't exist
      if ('reporting' in self.alert_source_meta) and \
          (reporting_state in self.alert_source_meta['reporting']) and \
          ('text' in self.alert_source_meta['reporting'][reporting_state]):
          res_base_text = self.alert_source_meta['reporting'][reporting_state]['text']

      if res_base_text is None:
        res_base_text = self._get_reporting_text(result_state)

    except Exception as exception:
      message = "[Alert][{0}] Unable to execute alert. {1}".format(
        self.get_name(), str(exception))
      
      # print the exception if in DEBUG, otherwise just log the warning
      if logger.isEnabledFor(logging.DEBUG):
        logger.exception(message)
      else:
        logger.warning(message)

      res = (BaseAlert.RESULT_UNKNOWN, [str(exception)])
      res_base_text = "{0}"
    
    
    if logger.isEnabledFor(logging.DEBUG):
      logger.debug("[Alert][{0}] result = {1}".format(self.get_name(), str(res)))
      
    data = {}
    data['name'] = self._get_alert_meta_value_safely('name')
    data['label'] = self._get_alert_meta_value_safely('label')
    data['state'] = res[0]
    data['text'] = res_base_text.format(*res[1])
    data['cluster'] = self.cluster_name
    data['service'] = self._get_alert_meta_value_safely('serviceName')
    data['component'] = self._get_alert_meta_value_safely('componentName')
    data['timestamp'] = long(time.time() * 1000)
    data['uuid'] = self._get_alert_meta_value_safely('uuid')
    data['enabled'] = self._get_alert_meta_value_safely('enabled')

    if logger.isEnabledFor(logging.DEBUG):
      logger.debug("[Alert][{0}] text = {1}".format(self.get_name(), data['text']))
    
    self.collector.put(self.cluster_name, data)


  def _get_configuration_value(self, key):
    """
    Gets the value of the specified configuration key from the cache. The key
    should be of the form {{foo-bar/baz}}. If the key is not a lookup key
    and is instead a constant, such as "foo" or "5", then the constant is
    returned.
    :return:
    """
    if key is None:
      return None

    # parse {{foo-bar/baz}}
    placeholder_keys = re.findall("{{([\S]+)}}", key)

    # if none found, then return the original
    if len(placeholder_keys) == 0:
      return key

    # this is a lookup key, so transform it into a value from the config cache
    placeholder_key = placeholder_keys[0]

    return self.cluster_configuration.get_configuration_value(
      self.cluster_name, placeholder_key)

    
  def _lookup_uri_property_keys(self, uri_structure):
    """
    Loads the configuration lookup keys that the URI structure needs. This
    will return a named tuple that contains the keys needed to lookup
    parameterized URI values from the cached configuration.
    The URI structure looks something like:
    
    "uri":{ 
      "http": foo,
      "https": bar,
      ...
    }
    """
    
    if uri_structure is None:
      return None
    
    http_key = None
    https_key = None
    https_property_key = None
    https_property_value_key = None
    default_port = None
    kerberos_keytab = None
    kerberos_principal = None
    ha_nameservice = None
    ha_alias_key = None
    ha_http_pattern = None
    ha_https_pattern = None
    
    if 'http' in uri_structure:
      http_key = uri_structure['http']
    
    if 'https' in uri_structure:
      https_key = uri_structure['https']
      
    if 'https_property' in uri_structure:
      https_property_key = uri_structure['https_property']
      
    if 'https_property_value' in uri_structure:
      https_property_value_key = uri_structure['https_property_value']

    if 'default_port' in uri_structure:
      default_port = uri_structure['default_port']

    if 'kerberos_keytab' in uri_structure:
      kerberos_keytab = uri_structure['kerberos_keytab']

    if 'kerberos_principal' in uri_structure:
      kerberos_principal = uri_structure['kerberos_principal']

    if 'high_availability' in uri_structure:
      ha = uri_structure['high_availability']

      if 'nameservice' in ha:
        ha_nameservice = ha['nameservice']

      if 'alias_key' in ha:
        ha_alias_key = ha['alias_key']

      if 'http_pattern' in ha:
        ha_http_pattern = ha['http_pattern']

      if 'https_pattern' in ha:
        ha_https_pattern = ha['https_pattern']


    AlertUriLookupKeys = namedtuple('AlertUriLookupKeys', 
      'http https https_property https_property_value default_port '
      'kerberos_keytab kerberos_principal '
      'ha_nameservice ha_alias_key ha_http_pattern ha_https_pattern')
    
    alert_uri_lookup_keys = AlertUriLookupKeys(http=http_key, https=https_key, 
      https_property=https_property_key,
      https_property_value=https_property_value_key, default_port=default_port,
      kerberos_keytab=kerberos_keytab, kerberos_principal=kerberos_principal,
      ha_nameservice=ha_nameservice, ha_alias_key=ha_alias_key,
      ha_http_pattern=ha_http_pattern, ha_https_pattern=ha_https_pattern
    )
    
    return alert_uri_lookup_keys

    
  def _get_uri_from_structure(self, alert_uri_lookup_keys):
    """
    Gets the URI to use by examining the URI structure from the definition.
    This will return a named tuple that has the uri and the SSL flag. The
    URI structure looks something like:
    
    "uri":{ 
      "http": foo,
      "https": bar,
      ...
    }
    """
    
    if alert_uri_lookup_keys is None:
      return None
    
    http_uri = None
    https_uri = None

    # first thing is first; if there are HA keys then try to dynamically build
    # the property which is used to get the actual value of the uri
    # (ie dfs.namenode.http-address.c1ha.nn2)
    if alert_uri_lookup_keys.ha_nameservice is not None or alert_uri_lookup_keys.ha_alias_key is not None:
      alert_uri = self._get_uri_from_ha_structure(alert_uri_lookup_keys)
      if alert_uri is not None:
        return alert_uri

    # attempt to parse and parameterize the various URIs; properties that
    # do not exist int he lookup map are returned as None
    if alert_uri_lookup_keys.http is not None:
      http_uri = self._get_configuration_value(alert_uri_lookup_keys.http)
    
    if alert_uri_lookup_keys.https is not None:
      https_uri = self._get_configuration_value(alert_uri_lookup_keys.https)

    # without a URI, there's no way to create the structure we need - return
    # the default port if specified, otherwise throw an exception
    if http_uri is None and https_uri is None:
      if alert_uri_lookup_keys.default_port is not None:
        alert_uri = AlertUri(uri=alert_uri_lookup_keys.default_port, is_ssl_enabled=False)
        return alert_uri
      else:
        raise Exception("Could not determine result. Either the http or https URI must be specified.")

    # start out assuming plaintext
    uri = http_uri
    is_ssl_enabled = False
    
    if https_uri is not None:
      # https without http implies SSL, otherwise look it up based on the properties
      if http_uri is None:
        is_ssl_enabled = True
        uri = https_uri
      elif self._check_uri_ssl_property(alert_uri_lookup_keys):
        is_ssl_enabled = True
        uri = https_uri
    
    alert_uri = AlertUri(uri=uri, is_ssl_enabled=is_ssl_enabled)
    return alert_uri


  def _get_uri_from_ha_structure(self, alert_uri_lookup_keys):
    """
    Attempts to parse the HA URI structure in order to build a dynamic key
    that represents the correct host URI to check.
    :param alert_uri_lookup_keys:
    :return: the AlertUri named tuple if there is a valid HA URL, otherwise None
    """
    if alert_uri_lookup_keys is None:
      return None

    logger.debug("[Alert][{0}] HA URI structure detected in definition, attempting to lookup dynamic HA properties".format(self.get_name()))

    ha_nameservice = self._get_configuration_value(alert_uri_lookup_keys.ha_nameservice)
    ha_alias_key = alert_uri_lookup_keys.ha_alias_key
    ha_http_pattern = alert_uri_lookup_keys.ha_http_pattern
    ha_https_pattern = alert_uri_lookup_keys.ha_https_pattern

    # at least one of these keys is needed
    if ha_nameservice is None and ha_alias_key is None:
      return None

    # convert dfs.ha.namenodes.{{ha-nameservice}} into
    # dfs.ha.namenodes.c1ha
    if ha_nameservice is not None:
      ha_alias_key = ha_alias_key.replace(self.HA_NAMESERVICE_PARAM, ha_nameservice)

    # grab the alias value which should be like nn1, nn2
    ha_nameservice_alias = self._get_configuration_value(ha_alias_key)
    if ha_nameservice_alias is None:
      logger.warning("[Alert][{0}] HA nameservice value is present but there are no aliases for {1}".format(
        self.get_name(), ha_alias_key))

      return None

    # determine which pattern to use (http or https)
    ha_pattern = ha_http_pattern
    is_ssl_enabled = self._check_uri_ssl_property(alert_uri_lookup_keys)
    if is_ssl_enabled:
      ha_pattern = ha_https_pattern

    # no pattern found
    if ha_pattern is None:
      logger.warning("[Alert][{0}] There is no matching http(s) pattern for the HA URI".format(
        self.get_name()))

      return None

    if self.HA_NAMESERVICE_PARAM in ha_pattern and ha_nameservice is None:
      logger.warning("[Alert][{0}] An HA URI pattern of {1} was detected, but there is no nameservice key".format(
        self.get_name(), ha_pattern))

      return None

    # convert dfs.namenode.http-address.{{ha-nameservice}}.{{alias}} into
    # dfs.namenode.http-address.c1ha.{{alias}}
    if ha_nameservice is not None:
      ha_pattern = ha_pattern.replace(self.HA_NAMESERVICE_PARAM, ha_nameservice)

    # for each alias, grab it and check to see if this host matches
    for alias in ha_nameservice_alias.split(','):
      # convert dfs.namenode.http-address.c1ha.{{alias}} into
      # dfs.namenode.http-address.c1ha.nn1
      key = ha_pattern.replace(self.HA_ALIAS_PARAM, alias.strip())

      # get the host for dfs.namenode.http-address.c1ha.nn1 and see if it's
      # this host
      value = self._get_configuration_value(key)
      if value is not None and self.host_name in value:
        return AlertUri(uri=value, is_ssl_enabled=is_ssl_enabled)

    return None


  def _check_uri_ssl_property(self, alert_uri_lookup_keys):
    """
    Gets whether the SSL property and value on the URI indicate an SSL
    connection.
    :param alert_uri_lookup_keys:
    :return:  True if the SSL check property and value are defined and match
              otherwise False
    """
    https_property = None
    https_property_value = None

    if alert_uri_lookup_keys.https_property is not None:
      https_property = self._get_configuration_value(alert_uri_lookup_keys.https_property)

    if alert_uri_lookup_keys.https_property_value is not None:
      https_property_value = self._get_configuration_value(alert_uri_lookup_keys.https_property_value)

    if https_property is None:
      return False

    return https_property == https_property_value


  def _collect(self):
    """
    Low level function to collect alert data.  The result is a tuple as:
    res[0] = the result code
    res[1] = the list of arguments supplied to the reporting text for the result code
    """  
    raise NotImplementedError


  def _get_reporting_text(self, state):
    '''
    Gets the default reporting text to use when the alert definition does not
    contain any. Subclasses can override this to return specific text.
    :param state: the state of the alert in uppercase (such as OK, WARNING, etc)
    :return:  the parameterized text
    '''
    return '{0}'

  """
  See RFC3986, Appendix B
  Tested on the following cases:
    "192.168.54.1"
    "192.168.54.2:7661
    "hdfs://192.168.54.3/foo/bar"
    "ftp://192.168.54.4:7842/foo/bar"

    Returns None if only a port is passed in
  """
  @staticmethod
  def get_host_from_url(uri):
    if uri is None:
      return None
    
    # if not a string, return None
    if not isinstance(uri, basestring):
      return None    
        
    # RFC3986, Appendix B
    parts = re.findall('^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?', uri)

    # index of parts
    # scheme    = 1
    # authority = 3
    # path      = 4
    # query     = 6
    # fragment  = 8

    host_and_port = uri
    if 0 == len(parts[0][1]):
      host_and_port = parts[0][4]
    elif 0 == len(parts[0][2]):
      host_and_port = parts[0][1]
    elif parts[0][2].startswith("//"):
      host_and_port = parts[0][3]

    if -1 == host_and_port.find(':'):
      if host_and_port.isdigit():
        return None    
      
      return host_and_port
    else:
      return host_and_port.split(':')[0]
