#!/usr/bin/python
###############################################################################
#                                                                             #
# ddns - A dynamic DNS client for IPFire                                      #
# Copyright (C) 2012 IPFire development team                                  #
#                                                                             #
# This program is free software: you can redistribute it and/or modify        #
# it under the terms of the GNU General Public License as published by        #
# the Free Software Foundation, either version 3 of the License, or           #
# (at your option) any later version.                                         #
#                                                                             #
# This program is distributed in the hope that it will be useful,             #
# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
# GNU General Public License for more details.                                #
#                                                                             #
# You should have received a copy of the GNU General Public License           #
# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
#                                                                             #
###############################################################################

import datetime
import logging
import os
import subprocess
import urllib2
import xml.dom.minidom

from i18n import _

# Import all possible exception types.
from .errors import *

logger = logging.getLogger("ddns.providers")
logger.propagate = 1

_providers = {}

def get():
	"""
		Returns a dict with all automatically registered providers.
	"""
	return _providers.copy()

class DDNSProvider(object):
	# A short string that uniquely identifies
	# this provider.
	handle = None

	# The full name of the provider.
	name = None

	# A weburl to the homepage of the provider.
	# (Where to register a new account?)
	website = None

	# A list of supported protocols.
	protocols = ("ipv6", "ipv4")

	DEFAULT_SETTINGS = {}

	# holdoff time - Number of days no update is performed unless
	# the IP address has changed.
	holdoff_days = 30

	# holdoff time for update failures - Number of days no update
	# is tried after the last one has failed.
	holdoff_failure_days = 0.5

	# True if the provider is able to remove records, too.
	# Required to remove AAAA records if IPv6 is absent again.
	can_remove_records = True

	# Automatically register all providers.
	class __metaclass__(type):
		def __init__(provider, name, bases, dict):
			type.__init__(provider, name, bases, dict)

			# The main class from which is inherited is not registered
			# as a provider.
			if name == "DDNSProvider":
				return

			if not all((provider.handle, provider.name, provider.website)):
				raise DDNSError(_("Provider is not properly configured"))

			assert not _providers.has_key(provider.handle), \
				"Provider '%s' has already been registered" % provider.handle

			_providers[provider.handle] = provider

	@staticmethod
	def supported():
		"""
			Should be overwritten to check if the system the code is running
			on has all the required tools to support this provider.
		"""
		return True

	def __init__(self, core, **settings):
		self.core = core

		# Copy a set of default settings and
		# update them by those from the configuration file.
		self.settings = self.DEFAULT_SETTINGS.copy()
		self.settings.update(settings)

	def __repr__(self):
		return "<DDNS Provider %s (%s)>" % (self.name, self.handle)

	def __cmp__(
self, other):
		return cmp(self.hostname, other.hostname)

	@property
	def db(self):
		return self.core.db

	def get(self, key, default=None):
		"""
			Get a setting from the settings dictionary.
		"""
		return self.settings.get(key, default)

	@property
	def hostname(self):
		"""
			Fast access to the hostname.
		"""
		return self.get("hostname")

	@property
	def username(self):
		"""
			Fast access to the username.
		"""
		return self.get("user
name")

	@property
	def password(self):
		"""
			Fast access to the password.
		"""
		return self.get("password")

	@property
	def token(self):
		"""
			Fast access to the token.
		"""
		return self.get("token")

	def __call__(self, force=False):
		if force:
			logger.debug(_("Updating %s forced") % self.hostname)

		# Do nothing if the last update has failed or no update is required
		elif self.has_failure or not self.requires_update:
			return

		# Execute the update.
		try:
			self.update()

		# Catch network errors early, because we do not want to log
		# them to the database. They are usually temporary and caused
		# by the client side, so that we will retry quickly.
		except DDNSNetworkError as e:
			raise

		# In case of any errors, log the failed request and
		# raise the exception.
		except DDNSError as e:
			self.core.db.log_failure(self.hostname, e)
			raise

		logger.info(_("Dynamic DNS update for %(hostname)s (%(provider)s) successful") % \
			{ "hostname" : self.hostname, "provider" : self.name })
		self.core.db.log_success(self.hostname)

	def update(self):
		for protocol in self.protocols:
			if self.have_address(protocol):
				self.update_protocol(protocol)
			elif self.can_remove_records:
				self.remove_protocol(protocol)

	def update_protocol(self, proto):
		raise NotImplementedError

	def remove_protocol(self, proto):
		if not self.can_remove_records:
			raise RuntimeError, "can_remove_records is enabled, but remove_protocol() not implemented"

		raise NotImplementedError

	@property
	def requires_update(self):
		# If the IP addresses have changed, an update is required
		if self.ip_address_changed(self.protocols):
			logger.debug(_("An update for %(hostname)s (%(provider)s)"
				" is performed because of an IP address change") % \
				{ "hostname" : self.hostname, "provider" : self.name })

			return True

		# If the holdoff time has expired, an update is required, too
		if self.holdoff_time_expired():
			logger.debug(_("An update for %(hostname)s (%(provider)s)"
				" is performed because the holdoff time has expired") % \
				{ "hostname" : self.hostname, "provider" : self.name })

			return True

		# Otherwise, we don't need to perform an update
		logger.debug(_("No update required for %(hostname)s (%(provider)s)") % \
			{ "hostname" : self.hostname, "provider" : self.name })

		return False

	@property
	def has_failure(self):
		"""
			Returns True when the last update has failed and no retry
			should be performed, yet.
		"""
		last_status = self.db.last_update_status(self.hostname)

		# Return False if the last update has not failed.
		if not last_status == "failure":
			return False

		# If there is no holdoff time, we won't update ever again.
		if self.holdoff_failure_days is None:
			logger.warning(_("An update has not been performed because earlier updates failed for %s") \
				% self.hostname)
			logger.warning(_("There will be no retries"))

			return True

		# Determine when the holdoff time ends
		last_update = self.db.last_update(self.hostname, status=last_status)
		holdoff_end = last_update + datetime.timedelta(days=self.holdoff_failure_days)

		now = datetime.datetime.utcnow()
		if now < holdoff_end:
			failure_message = self.db.last_update_failure_message(self.hostname)

			logger.warning(_("An update has not been performed because earlier updates failed for %s") \
				% self.hostname)

			if failure_message:
				logger.warning(_("Last failure message:"))

				for line in failure_message.splitlines():
					logger.warning("  %s" % line)

			logger.warning(_("Further updates will be withheld until %s") % holdoff_end)

			return True

		return False

	def ip_address_changed(self, protos):
		"""
			Returns True if this host is already up to date
			and does not need to change the IP address on the
			name server.
		"""
		for proto in protos:
			addresses = self.core.system.resolve(self.hostname, proto)
			current_address = self.get_address(proto)

			# Handle if the system has n