#!/usr/bin/env python
# Copyright 2009 Google Inc. All Rights Reserved.
#
# Licensed 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.

"""Module for all nameserver related activity."""

__author__ = 'tstromberg@google.com (Thomas Stromberg)'

import random
import re
import socket
import sys
import time

# external dependencies (from nb_third_party)
import dns.exception
import dns.message
import dns.name
import dns.query
import dns.rcode
import dns.rdataclass
import dns.rdatatype
import dns.resolver
import dns.reversename
import dns.version

import health_checks
import provider_extensions
import addr_util
import util

# Look for buggy system versions of namebench
if dns.version.hexversion < 17301744:
  raise ValueError('dnspython 1.8.0+ required, while only %s was found. The '
                   'namebench source bundles 1.8.0, so use it.' % dns.version.version)



# How many failures before we disable system nameservers
MAX_NORMAL_FAILURES = 2
MAX_KEEPER_FAILURES = 8
MAX_WARNINGS = 10
FAILURE_PRONE_RATE = 10

# In order of most likely to be important.
PROVIDER_TAGS = ['isp', 'network', 'likely-isp', 'dhcp']

# EVIL IMPORT-TIME SIDE EFFECT
BEST_TIMER_FUNCTION = util.GetMostAccurateTimerFunction()


def ResponseToAscii(response):
  if not response:
    return None
  if response.answer:
    answers = [', '.join(map(str, x.items)) for x in response.answer]
    return ' -> '.join(answers).rstrip('"').lstrip('"')
  else:
    return dns.rcode.to_text(response.rcode())


class BrokenSystemClock(Exception):
  """Used if we detect errors with the system clock."""
  def __init__(self, value):
    self.value = value

  def __str__(self):
    return repr(self.value)


class NameServer(health_checks.NameServerHealthChecks, provider_extensions.NameServerProvider):
  """Hold information about a particular nameserver."""

  def __init__(self, ip, hostname=None, name=None, tags=None, provider=None,
               instance=None, location=None, latitude=None, longitude=None, asn=None,
               network_owner=None, dhcp_position=None, system_position=None):
    self.ip = ip
    self.name = name
    self.dhcp_position = dhcp_position
    self.system_position = system_position

    if tags:
      self.tags = set(tags)
    else:
      self.tags = set()
    self.provider = provider
    self.instance = instance
    self.location = location

    if self.location:
      self.country_code = location.split('/')[0]
      self.tags.add('country_%s' % self.country_code.lower())
    else:
      self.country_code = None

    self.latitude = latitude
    self.longitude = longitude
    self.asn = asn
    self.network_owner = network_owner
    self._hostname = hostname

    self.timeout = 5
    self.health_timeout = 5
    self.ping_timeout = 1
    self.ResetTestStatus()
    self._version = None
    self._node_ids = set()

    self.timer = BEST_TIMER_FUNCTION

    if ':' in sel
f.ip:
      self.tags.add('ipv6')
    elif '.' in 
self.ip:
      self.tags.add('ipv4')

    if self.dhcp_position is not None:
      self.tags.add('dhcp')
    if self.system_position is not None:
      self.tags.add('system')

    if ip.endswith('.0') or ip.endswith('.255'):
      self.DisableWithMessage("IP appears to be a broadcast address.")
    elif self.is_bad:
      self.DisableWithMessage("Known bad address.")

  def AddNetworkTags(self, domain, provider, asn, country_code):
    if self.hostname:
      my_domain = addr_util.GetDomainFromHostname(self.hostname)
      hostname = self.hostname.lower()
    else:
      my_domain = 'UNKNOWN'
      hostname = ''
    if provider:
      provider = provider.lower()

    if domain and my_domain == domain:
      self.tags.add('isp')
    if asn and self.asn == asn:
      self.tags.add('network')

      if provider and 'isp' not in self.tags:
        if (provider in self.name.lower() or provider in self.hostname.lower()
            or (self.network_owner and provider in self.network_owner.lower())):
          self.tags.add('isp')
    elif provider and self.country_code == country_code and my_domain != domain:
      if (provider in self.name.lower() or provider in hostname
        or (self.network_owner and provider in self.network_owner.lower())):
        self.tags.add('likely-isp')

  def ResetTestStatus(self):
    """Reset testing status of this host."""
    self.warnings = set()
    self.shared_with = set()
    if self.is_disabled:
      self.tags.remove('disabled')
    self.checks = []
    self.failed_test_count = 0
    self.share_check_count = 0
    self.cache_checks = []
    self.is_slower_replica = False
    self.ResetErrorCounts()

  def ResetErrorCounts(self):
    """NOTE: This gets called by benchmark.Run()!"""

    self.request_count = 0
    self.failure_count = 0
    self.error_map = {}

  @property
  def is_keeper(self):
    return bool(self.MatchesTags(['preferred', 'dhcp', 'system', 'specified']))

  @property
  def is_bad(self):
    if not self.is_keeper and self.MatchesTags(['rejected', 'blacklist']):
      return True

  @property
  def is_hidden(self):
    return self.HasTag('hidden')

  @property
  def is_disabled(self):
    return self.HasTag('disabled')

  @property
  def check_average(self):
    # If we only have a ping result, sort by it. Otherwise, use all non-ping results.
    if len(self.checks) == 1:
      return self.checks[0][3]
    else:
      return util.CalculateListAverage([x[3] for x in self.checks[1:]])

  @property
  def fastest_check_duration(self):
    if self.checks:
      return min([x[3] for x in self.checks])
    else:
      return 0.0

  @property
  def check_duration(self):
    return sum([x[3] for x in self.checks])

  @property
  def warnings_string(self):
    if self.is_disabled:
      return 'DISABLED: %s' % self.disabled_msg
    else:
      return ', '.join(map(str, self.warnings))

  @property
  def errors(self):
    return ['%s (%s requests)' % (_[0], _[1]) for _ in self.error_map.items() if _[0] != 'Timeout']

  @property
  def error_count(self):
    return sum([_[1] for _ in self.error_map.items() if _[0] != 'Timeout'])

  @property
  def timeout_count(self):
    return self.error_map.get('Timeout', 0)

  @property
  def notes(self):
    """Return a list of notes about this nameserver object."""
    my_notes = []
    if self.system_position == 0:
      my_notes.append('The current preferred DNS server')
    elif self.system_position:
      my_notes.append('A backup DNS server for this system')
    if self.dhcp_position is not None:
      my_notes.append('Assigned by your network DHCP server')
    if self.is_failure_prone:
      my_notes.append('%s of %s queries failed' % (self.failure_count, self.request_count))
    if self.HasTag('blacklist'):
      my_notes.append('BEWARE: IP appears in DNS server blacklist')
    if self.is_disabled:
      my_notes.append(self.disabled_msg)
    else:
      my_notes.extend(self.warnings)
    if self.errors:
      my_notes.extend(self.errors)
    return my_notes

  @property
  def hostname(self):
    if self._hostname is None and not self.is_disabled:
      self.UpdateHostname()
    return self._hostname

  def UpdateHostname(self):
    if not self.is_disabled:
      self._hostname = self.GetReverseIp(self.ip)
    return self._hostname

  @property
  def version(self):
    if self._version is None and not self.is_disabled:
      self.GetVersion()

    if not self._version:
      return None

    # Only return meaningful data
    if (re.search('\d', self._version) or
        (re.search('recursive|ns|server|bind|unbound', self._version, re.I)
         and 'ontact' not in self._version and '...' not in self._version)):
      return self._version
    else:
      return None

  @proper