"""A framework, that verifies availability of hosting resources.

Each found problem is assigned a severity, a type, that uniquely identifies the
kind of the problem, and a message text, that has detailed information on the
object being tested.

Interfaces, that framework users should implement, are defined in __init__.py (this file)

An overview of the package contents:

parallels/hosting_check
|-- checkers				# Source code, that makes hosting checks
|   |-- dns_checker.py
|   |-- dns_external_checker.py
	[...]
|-- __init__.py				# Interfaces, constants, common classes
|-- messages				# Error messages, that hosting checks can produce
|   |-- __init__.py
|   |-- problem				# Description of the found problems, per hosting component
|   |   |-- dns_external.py
|   |   |-- dns_forwarding.py
		[...]
|   |   `-- website_availability.py
|   |-- solutions			# Description of the proposed solutions for the problems found
|   |   |-- __init__.py
|   |   |-- plesk			# Plesk panel specific solutions
|   |   |   |-- dns_external.py
|   |   |   |-- dns.py
			[...]
|   |   |   `-- website_availability.py
|   |   `-- ppa				# PPA panel specific solutions
|   |       |-- dns_external.py
|   |       |-- dns_forwarding.py
			[...]
|   |       `-- website_availability.py
|   `-- utils.py
|-- panel_services.py
|-- scripts
|-- signatures.py
|-- utils
`-- web_signatures.py
"""

from parallels.hosting_check.utils.issue_types import IssueIdsSet
from parallels.hosting_check.utils.entity_meta import \
		Entity, Property, \
		PropertyType, PropertyTypeString, \
		PropertyTypeList, PropertyTypeEntity

# ========================================================
# Checker's interface
# ========================================================

class DomainServicesHostingCheckerInterface(object):
	"""Interface class"""

	def check_website_availability(self, domains_to_check, website_availability_check_timeout=30):
		"""Check whether the index pages of specified domains can be retrieved from the specified IP addresses over HTTP and, optionally, HTTPS.
		   @param domains_to_check	list of items describing what should be checked.
		   @param website_availability_check_timeout	timeout on site response.
		   @return			list of DomainIssue items describing the problems occurred.
		   For possible issue types check 'WebsiteAvailabilityIssueType'
		"""
		raise NotImplementedError()

	def check_dns(
		self, domains_to_check, 
		max_difference_lines=3, save_external_report_data_function=None, 
		dns_server_name='PPA DNS server',
		difference_file_panel_id='ppa'
	):
		"""Check whether all necessary DNS records of the specified domains are returned by specified DNS servers.
		   @param domains_to_check	list of DomainDNSService items describing what should be checked.
		   @return			list of DomainIssue items describing the problems occurred.
		   For possible issue types check 'DNSIssueType'
		"""
		raise NotImplementedError()

	def check_mail_auth(self, domains_to_check, messages_delta_limit=5):
		"""Check whether all mail users of the specified domains can authenticate over IMAP and SMTP on the specified mail server.
		   @param domains_to_check	list of DomainMailService items describing what should be checked.
		   @messages_delta_limit	number of messages is bot transported to consider it critical
		   @return			list of DomainIssue items describing the problems occurred.
		   For possible issue types check 'MailAuthIssueType'
		"""
		raise NotImplementedError()

	def check_ftp_auth(self, domains_to_check):
		"""Check whether all system users can authenticate over FTP on the specified web server.
		   @param domains_to_check	list of DomainFTPService items describing what should be checked.
		   @return			list of DomainIssue items describing the problems occurred.
		   For possible issue types check 'FTPAuthIssueType'
		"""
		raise NotImplementedError()
# ========================================================
# Data types you have to pass to the checker or get from the checker
# ========================================================

class DomainIssue(object):
	"""This structure describes the items of the returned list of issues."""
	def __init__(self, domain_name, category, severity, problem):
		"""Arguments:
		- domain_name - [mandatory] Domain name. Type: unicode string.
		- category - [mandatory] Category of the problem, intended for sorting
		  and ordering the similar problems. Type: string.
		- severity - [mandatory] Severity of the problem. Type: string.
		- problem - [mandatory] Problem statement: what is wrong, how it
		  impacts against the goal. Type: string."""
		self.domain_name = domain_name
		self.category = category
		self.severity = severity
		self.problem = problem

	def __repr__(self):
		return "DomainIssue(domain_name=%r, category=%r, severity=%r, problem=%r)" % (
			self.domain_name, self.category, self.severity, self.problem
		)

	def __eq__(self, another):
		return (
			self.domain_name == another.domain_name and
			self.category == another.category and
			self.severity == another.severity and
			self.problem == another.problem
		)
class ServiceIssue(object):
	"""This structure describes the items of the returned list of issues."""
	def __init__(self, category, severity, problem):
		"""Arguments:
		- category - [mandatory] Category of the problem, intended for sorting
		  and ordering the similar problems. Type: string.
		- severity - [mandatory] Severity of the problem. Type: string.
		- problem - [mandatory] Problem statement: what is wrong, how it
		  impacts against the goal. Type: string."""
		self.category = category
		self.severity = severity
		self.problem = problem

	def __repr__(self):
		return "ServiceIssue(category=%r, severity=%r, problem=%r)" % (
			self.category, self.severity, self.problem
		)

	def __eq__(self, another):
		return (
			self.category == another.category and
			self.severity == another.severity and
			self.problem == another.problem
		)

class Severity(object):
	"""Supported severity values."""

	INFO = 'info'
	WARNING = 'warning'
	ERROR = 'error'

issue_ids = IssueIdsSet()

class ServiceIssueType:
	"""Issue types when checking service"""

	# Service is not started on host
	SERVICE_NOT_STARTED = issue_ids.unique('service_not_started')
	# Service port is closed on target server
	SERVICE_PORT_IS_CLOSED = issue_ids.unique('service_port_is_closed')
	# Unable connect to service by port from local server
	SERVICE_CONNECTION_ERROR = issue_ids.unique('service_connection_error')
	# Unexpected internal error when checking service
	SERVICE_INTERNAL_ERROR = issue_ids.unique('service_internal_error')

class MailAuthIssueType:
	"""Issue types when checking mail auth"""

	# Domain has no mail IP address, so we can not check it
	MAIL_IP_NOT_DEFINED = issue_ids.unique('mail_ip_not_defined')
	# Mailuser does not exist on target panel
	MAILUSER_NOT_EXIST = issue_ids.unique('mailuser_not_exist')
	# Failed to connect, failed to login or any other netword/protocol issue when checking login by IMAP
	CHECK_FAILED_IMAP = issue_ids.unique('connection_failed_imap')
	# Unexpected internal error when checking login by IMAP
	INTERNAL_ERROR_IMAP = issue_ids.unique('internal_error_imap')
	# Failed to connect, failed to login or any other netword/protocol issue when checking login by SMTP
	CHECK_FAILED_SMTP = issue_ids.unique('connection_failed_smtp')
	# Unexpected internal error when checking login by SMTP
	INTERNAL_ERROR_SMTP = issue_ids.unique('internal_error_smtp')
	# Failed to connect, failed to login or any other netword/protocol issue when checking login by POP3
	CHECK_FAILED_POP3 = issue_ids.unique('connection_failed_pop3')
	# Unexpected internal error when checking login by POP3
	INTERNAL_ERROR_POP3 = issue_ids.unique('internal_error_pop3')
	# Failed to connect, failed to login or any other netword/protocol issue when counting messages by POP3
	MESSAGES_COUNT_POP3_ERROR = issue_ids.unique('messages_count_pop3_error')
	# Unexpected internal error when counting messages by POP3
	MESSAGES_COUNT_INTERNAL_ERROR = issue_ids.unique('messages_count_internal_error')
	# After copy mail content has significant difference in count messages
	MESSAGES_COUNT_SIGNIFICANT_DIFFERENCE = issue_ids.unique('messages_count_significant_difference')
	# After copy mail content has insignificant difference in count messages
	MESSAGES_COUNT_INSIGNIFICANT_DIFFERENCE = issue_ids.unique('messages_count_insignificant_difference')

class FTPAuthIssueType:
	"""Issue types when checking FTP auth"""

	# Domain has no web IP address, so we can not check it
	WEB_IP_NOT_DEFINED = issue_ids.unique('web_ip_not_defined')
	# Failed to connect, failed to login or any other network/protocol issue when checking login by FTP
	CHECK_FAILED = issue_ids.unique('check_failed')
	# Was able to connect and login as inactive user by FTP
	INACTIVE_USER_CAN_CONNECT = issue_ids.unique('ftp_auth_check_inactive_success')
	# Unexpected internal error when checking login by FTP
	INTERNAL_ERROR = issue_ids.unique('ftp_internal_error')
	# User password is encrypted
	ENCRYPTED_PASSWORD = issue_ids.unique('encrypted_ftp_password')

class SSHAuthIssueType:
	"""Issue types when checking SSH auth"""

	# Failed to connect, failed to login or any other network/protocol issue
	# when checking login for active user by SSH
	CHECK_FAILED = issue_ids.unique('ssh_auth_check_failed')
	# Was able to connect and login as inactive user by SSH
	INACTIVE_USER_CAN_CONNECT = issue_ids.unique('ssh_auth_check_inactive_success')
	# User password is encrypted
	ENCRYPTED_PASSWORD = issue_ids.unique('encrypted_ssh_password')
	# Unexpected internal error when checking login by SSH
	INTERNAL_ERROR = issue_ids.unique('ssh_internal_error')

class RDPAuthIssueType:
	"""Issue types when checking RDP auth"""
	# Domain has no web IP address, so we can not check it
	WEB_IP_NOT_DEFINED = issue_ids.unique('rdp_web_ip_not_defined')
	# User is not configured to have RDP access
	ACTIVE_USER_NOT_CONFIGURED = issue_ids.unique('rdp_active_user_not_configured')
	# Inactive user is configured to have RDP connections allowed
	INACTIVE_USER_CONFIGURED = issue_ids.unique('rdp_inactive_user_configured')
	# Unexpected internal error when checking login by RDP 
	INTERNAL_ERROR = issue_ids.unique('rdp_auth_internal_error')

class DNSIssueType:
	"""Issue types when checking DNS"""

	# DNS server returned records that differ from expected
	DIFFERENT_RECORDS = issue_ids.unique('different_records')
	# Request to DNS server times out
	DNS_SERVER_TIMEOUT = issue_ids.unique('dns_server_timeout')
	# Unexpected internal error when checking DNS records
	INTERNAL_ERROR = issue_ids.unique('dns_internal_error')


# Issue types when checking DNS forwarding
class DNSForwardingIssueType:
	# DNS server returned records that differ from expected
	DIFFERENT_RECORDS = issue_ids.unique('dns_forwarding_different_records')
	# Request to DNS server timed out
	DNS_SERVER_TIMEOUT = issue_ids.unique('dns_forwarding_server_timeout')
	# Unexpected internal error when checking DNS records
	INTERNAL_ERROR = issue_ids.unique('dns_forwarding_internal_error')

# Issue types when checking DNS via external DNS servers
class DNSExternalIssueType:
	# Domain name is not registered, external DNS servers know nothing about it
	NOT_REGISTERED = issue_ids.unique('domain_not_registered')
	# Domain is delegated to the DNS servers for which we were not able to IPs
	SERVED_BY_UNKNOWN_DNS_SERVERS = issue_ids.unique('serverd_by_unknown_dns_servers')
	# Domain is not delegated to target or source panel servers, it is served by some another external DNS servers. So, it makes no sense to perfor check with external DNS.
	NOT_SERVED_BY_SOURCE_OR_TARGET = issue_ids.unique('not_served_by_source_or_target')
	# external DNS server returned records different than target DNS server
	DIFFERENT_RECORDS = issue_ids.unique('dns_external_different_records')
	# Request to DNS server timed out
	DNS_SERVER_TIMEOUT = issue_ids.unique('dns_external_server_timeout')
	# Unexpected internal error when checking DNS with external DNS
	INTERNAL_ERROR = issue_ids.unique('dns_external_internal_error')

class PostgreSQLDatabaseIssueType:
	# Database user does not exist on the target PostgreSQL database server
	DATABASE_USER_DOES_NOT_EXIST_IN_PANEL = \
		issue_ids.unique('pgsql_database_user_does_not_exist_in_panel')
	# Database does not exist on the target panel
	DATABASE_DOES_NOT_EXIST_IN_PANEL = \
		issue_ids.unique('pgsql_database_does_not_exist_in_panel')
	# Failed to fetch list of tables from the source PostgreSQL server. That
	# means that we will skip comparison of database table sets.
	FAILED_TO_FETCH_TABLES_FROM_SOURCE = \
		issue_ids.unique('pgsql_failed_to_fetch_tables_source')
	# Failed to fetch list of tables from the target PostgreSQL server. That
	# means that we will skip comparison of database table sets.
	FAILED_TO_FETCH_TABLES_FROM_TARGET = \
		issue_ids.unique('pgsql_failed_to_fetch_tables_target')
	# Target server has less database tables than source server, looks like
	# some tables were not migrated
	DIFFERENT_TABLES_SET = issue_ids.unique('pgsql_different_table_set')
	# Failed to execute simple SQL query as database user. Most probably, login
	# failed for that user.
	FAILED_TO_EXECUTE_SIMPLE_QUERY_AS_USER = \
		issue_ids.unique('pgsql_failed_to_execute_simple_query_as_user')
	# Error when connecting to PostgreSQL database server
	CONNECTION_ERROR = issue_ids.unique('pgsql_connection_error')
	# Database does not exist on the target PostgreSQL database server
	DATABASE_DOES_NOT_EXIST = issue_ids.unique('pgsql_database_does_not_exist')
	# PostgreSQL client (psql) generic error (not covered by other issue types)
	PSQL_CLIENT_GENERIC_ERROR = \
		issue_ids.unique('pgsql_client_generic_error')
	# Unexpected internal error when checking PostgreSQL databases
	INTERNAL_ERROR = issue_ids.unique('pgsql_internal_error')

class MySQLDatabaseIssueType:
	# Target server has less database tables than source server, looks like some tables were not migrated
	DIFFERENT_TABLES_SET = issue_ids.unique('mysql_different_table_set')
	# Failed to execute simple SQL query as database user. Most probably, login failed for that user.
	FAILED_TO_EXECUTE_SIMPLE_QUERY_AS_USER=issue_ids.unique('mysql_failed_to_execute_simple_query_as_user')
	# Database user does not exist on the target MySQL database server
	DATABASE_USER_DOES_NOT_EXIST_IN_PANEL = issue_ids.unique('mysql_database_user_does_not_exist_in_panel')
	# Error when connecting to MySQL database server
	CONNECTION_ERROR = issue_ids.unique('mysql_connection_error')
	# Database does not exist on the target panel
	DATABASE_DOES_NOT_EXIST_IN_PANEL = issue_ids.unique('mysql_database_does_not_exist_in_panel')
	# Database does not exist on the target MySQL database server
	DATABASE_DOES_NOT_EXIST = issue_ids.unique('mysql_database_does_not_exist')
	# MySQL client generic error (not covered by other issue types)
	MYSQL_CLIENT_GENERIC_ERROR = issue_ids.unique('mysql_client_generic_error')
	# Failed to fetch list of tables from the source MySQL server. That means that we will skip comparison of database table sets.
	FAILED_TO_FETCH_TABLES_FROM_SOURCE = issue_ids.unique('mysql_failed_to_fetch_tables_from_source')
	# User password is encrypted
	ENCRYPTED_PASSWORD = issue_ids.unique('mysql_encrypted_password')
	# Unexpected internal error when checking MySQL databases
	INTERNAL_ERROR = issue_ids.unique('mysql_internal_error')

class MSSQLDatabaseIssueType:
	# Target server has less database tables than source server, looks like some tables were not migrated
	DIFFERENT_TABLES_SET = issue_ids.unique('mssql_different_table_set')
	# Database user does not exist on the target MSSQL database server
	DATABASE_USER_DOES_NOT_EXIST_IN_PANEL = issue_ids.unique('mssql_database_user_does_not_exist_in_panel')
	# Database does not exist on the target panel
	DATABASE_DOES_NOT_EXIST_IN_PANEL = issue_ids.unique('mssql_database_does_not_exist_in_panel')
	# Failed to login to target server as admin. Database was not specified when trying to login, 
	# so most probably that is some connectivity issue or issue with the MSSQL server.
	FAILED_TO_LOGIN_TO_TARGET_SERVER_AS_ADMIN = issue_ids.unique('mssql_failed_to_login_to_target_server_as_admin')
	# Failed to login to target server as admin under database. Most probable reason: database does not exist.
	FAILED_TO_LOGIN_TO_TARGET_DATABASE_AS_ADMIN = issue_ids.unique('mssql_failed_to_login_to_target_database_as_admin')
	# Failed to login to the target MSSQL server as database user
	FAILED_TO_LOGIN_AS_USER = issue_ids.unique('mssql_failed_to_login_as_user')
	# Failed to fetch list of tables from the source MSSQL server. That means that we will skip comparison of database table sets.
	FAILED_TO_FETCH_TABLES_FROM_SOURCE = issue_ids.unique('mssql_failed_to_fetch_tables_from_source')
	# Unexpected internal error when checking MSSQL databases
	INTERNAL_ERROR = issue_ids.unique('mssql_internal_error')

class WebsiteAvailabilityIssueType:
	"""Issue types when checking web."""

	# Web server returned 5xx status code
	STATUS_CODE_5xx = issue_ids.unique('bad_status_code_5xx')
	# Web server returned 4xx status code
	STATUS_CODE_4xx = issue_ids.unique('bad_status_code_4xx')
	# Source and target servers returned different status codes
	# (for example, source returned '302' redirection, 
	# target returned '200' ok, which could mean that forwarding was broken)
	DIFFERENT_STATUS_CODE = issue_ids.unique('different_status_code')
	# Connection issue when connecting to web server
	CONNECTION_ISSUE = issue_ids.unique('connection_issue')
	# HTTP client failed to complete request or retrieve a response
	FAILED_HTTP_REQUEST = issue_ids.unique('http_request_issue')
	# Unexpected internal error when checking web index page
	INTERNAL_ERROR = issue_ids.unique('web_internal_error')
	# Unable to get source site to perform additional checks
	# That means that advanced checks that require 
	# source server will be skipped, only basic set of checks
	# will be performed
	UNABLE_TO_GET_SOURCE_SITE = issue_ids.unique('unable_to_get_source_site')
	# Source and target servers returned pages with different
	# titles (<title> HTML tag)
	DIFFERENT_TITLES = issue_ids.unique('different_titles')
	# Cannot decode URL: 'href' attribute should be urlencoded ASCII string
	CANNOT_DECODE_URL = issue_ids.unique('cannot_decode_url')

try:
	# Web signatures can be customized by providing 'web_signatures_custom.py'
	# which supersedes 'web_signatures.py' file. We need two separate files to
	# avoid overwrite of customizations during tool upgrade
	from parallels.hosting_check.web_signatures_custom import \
		WebIssueSignatureType, WEB_SIGNATURES
except ImportError:
	from parallels.hosting_check.web_signatures import \
		WebIssueSignatureType, WEB_SIGNATURES

# Just import WebIssueSignatureType from web_signatures module to get unified
# interface for all issue types. The statement below is necessary for lint
# tools (for example pyflakes) not to complain about unused import
WebIssueSignatureType = WebIssueSignatureType

class WebProtocol:
	"""Protocols to check web service on"""

	HTTP = 'http'
	HTTPS = 'https'

class Service(Entity):
	"""
	Properties:
	- host - [mandatory] Service host ip
	- description - [mandatory] Service host description
	- runner - [mandatory] Object of the class 'ServerRunnerBase' which can
	  work with the target Windows web node
	- service - [mandatory] Object of 'ServiceInfo' which describes a
	  checked service
	"""
	properties = [
		Property('host', PropertyTypeString()), 
		Property('description', PropertyTypeString()), 
		Property('runner', PropertyType()), 
		Property('service', PropertyType()),
	]

class ProcessInfo(Entity):
	"""
	Properties:
	- names - [mandatory] - List names of process which run by service
	- ports - [mandatory]  - List of ports which used by service
	"""
	properties = [
		Property('names', PropertyTypeList(PropertyTypeString())),
		Property('ports', PropertyTypeList(PropertyTypeString()))
	]

class ServiceInfo(Entity):
	"""
	Properties:
	- type - [mandatory] Type of server: web, ftp, mail, database or dns
	- processes - [mandatory] - List of ProcessInfo
	- is_windows - [mandatory] - Is service work on windows
	- check_connection - [mandatory] - Is check connection remotely
	"""
	properties = [
		Property('type', PropertyTypeString()),
		Property('processes', PropertyTypeList(PropertyTypeEntity(ProcessInfo))),
		Property('is_windows', PropertyTypeString()),
		Property('check_connection', PropertyTypeString())
	]

class DomainWebService(Entity):
	"""
	Properties:
	- domain_name - [mandatory] Domain name.
	- web_server_ip - [mandatory] IP address of the web server.
	- protocol - [mandatory] protocol to check: 'http' or 'https'.
	- source_web_server_ip - [optional] IP address of the web server domains
	  was migrated from. If specified, additional comparison of website on
	  source and on target servers is performed, to make sure site did not get
	  broken. If None is passed (or this option is not specified), checkers
	  won't compare pages on the target server with pages on the source server
	- signatures - [optional] list of signatures to look for in a page.  Type:
	  instance of Signature class. If None is passed (or this option is not
	  specified), default set of signatures is used.
	- aps_domain_relative_urls - [optional] list of APS applications relative
	  URLs to check. If None is passed (or this option is not specified), APS
	  URLs are not checked.
	- runner - [optional] an object of the class 'ServerRunnerBase' which
	  implements 'sh_unchecked' function. If None is passed (or this option is
	  not specified), checkers won't search for signatures in virtual host
	  error logs
	- error_logs - [optional] a list, that contains full paths to error log
	  files. If None is passed (or this option is not specified), checkers
	  won't search for signatures in virtual host error logs
	"""
	properties = [
		Property('domain_name', PropertyTypeString()),
		Property('web_server_ip', PropertyTypeString()),
		Property('protocol', PropertyTypeString()),
		Property('source_web_server_ip', PropertyTypeString()),
		Property('signatures', 
			PropertyType(),
			create_default=lambda: WEB_SIGNATURES
		),
		Property(
			'aps_domain_relative_urls', 
			PropertyTypeList(PropertyTypeString()), 
			create_default=lambda: []
		),
		Property('runner', PropertyType()),
		Property('error_logs', PropertyTypeList(PropertyTypeString())),
	]

class DNSRecord(Entity):
	"""
	Properties:
	- rec_type - [mandatory] Record type.
	- src - [mandatory] Record source.
	- dst - [mandatory] Record destination.
	- opt - [optional] Record's opt value (such as MX priority). 
		Specify the opt value or None.
	"""
	properties = [
		Property('rec_type', PropertyTypeString()),
		Property('src', PropertyTypeString()),
		Property('dst', PropertyTypeString()),
		Property('opt', PropertyTypeString())
	]

class DomainDNSService(Entity):
	"""
	Properties:
	- domain_name - [mandatory] Domain name.
	- dns_records - [mandatory] The DNS records that must be returned in 
		response to query for domain.
	- dns_servers - [mandatory] The DNS servers to query about this domain.
	"""
	properties = [
		Property('domain_name', PropertyTypeString()),
		Property(
			'dns_records', 
			PropertyTypeList(PropertyTypeEntity(DNSRecord))
		),
		Property('dns_servers', PropertyTypeList(PropertyTypeString())),
	]

class DomainDNSForwarding(Entity):
	"""
	Properties:
	- domain_name - [mandatory] Domain name.
	- dns_records - [mandatory] The DNS records that must be returned in
	  response to query for domain.
	- source_dns_servers - [mandatory] The DNS servers to query about this
	  domain.
	- target_dns_server - [mandatory] The DNS "reference" DNS server that
	  source DNS servers.
	"""
	properties = [
		Property('domain_name', PropertyTypeString()),	
		Property(
			'dns_records', 
			PropertyTypeList(PropertyTypeEntity(DNSRecord))
		),
		Property('source_dns_servers', PropertyTypeList(PropertyTypeString())),
		Property('target_dns_server', PropertyTypeList(PropertyTypeString())),
	]

class DomainDNSExternal(Entity):
	"""
	Properties:
	- domain_name - [mandatory] Domain name.
	- dns_records - [mandatory] The DNS records that must be returned in
	  response to query for domain.
	- source_dns_servers - [mandatory] The DNS servers to query about this
	  domain.
	- target_dns_servers - [mandatory] The DNS "reference" DNS server that
	  source DNS servers.
	"""
	properties = [
		Property('domain_name', PropertyTypeString()),	
		Property(
			'dns_records', 
			PropertyTypeList(PropertyTypeEntity(DNSRecord))
		),
		Property('source_dns_servers', PropertyTypeList(PropertyTypeString())),
		Property('target_dns_servers', PropertyTypeList(PropertyTypeString())),
	]

class User(Entity):
	"""
	Properties:
	- login - [mandatory] Login. Do not include the domain name here.
	- password - [mandatory] Password. 
	"""
	properties = [
		Property('login', PropertyTypeString()),
		Property('password', PropertyTypeString()),
		Property('password_type', PropertyTypeString()),
	]

class DomainMailService(Entity):
	"""
	Properties:
	- domain_name - [mandatory] Domain name.
	- mail_server_ip - [mandatory] IP address of the mail server.
	- source_mail_server_ip - [optional] IP address of the source mail server.
	- users - [mandatory] Mail users with their passwords as we expect them to
	  be restored on target panel.
	- source_users - [optional] Mail users with their password as we expect to
	  have them on source panel. If None is passed (or this option is not
	  specified), checks that require access to source server are skipped
	- target_panel_mail_users - [optional] Mail user logins that actually exist
	  on target panel for that domain. If None is passed (or this option is not
	  specified), check of panel mail users is skipped. Empty list ('[]')
	  should be passed if there are no mail users found on target panel.
	"""
	properties = [
		Property('domain_name', PropertyTypeString()),
		Property('mail_server_ip', PropertyTypeString()),
		Property('source_mail_server_ip', PropertyTypeString()),
		Property('users', PropertyTypeList(PropertyTypeEntity(User))),
		Property('source_users', PropertyTypeList(PropertyTypeEntity(User))),
		Property('target_panel_mail_users', PropertyTypeList(PropertyTypeString()))
	]

class DomainFTPService(Entity):
	"""
	Properties:
	- domain_name - [mandatory] Domain name. 
	- web_server_ip - [mandatory] Users to check the authentication as.
	- users - [mandatory] IP address of the web server. 
	- inactive_users - [optional] Users to check that authentication is
	  impossible
	"""
	properties = [
		Property('domain_name', PropertyTypeString()),
		Property('web_server_ip', PropertyTypeString()),
		Property('users', PropertyTypeList(PropertyTypeEntity(User))),
		Property('inactive_users', PropertyTypeList(PropertyTypeEntity(User)))
	]

class DomainSSHService(Entity):
	"""
	Properties:
	- domain_name - [mandatory] Domain name. 
	- web_server_ip - [mandatory] IP address of the Unix web server.
	- users - [mandatory] Users to check the authentication as.
	- inactive_users - [optional] Users to check that authentication is
	  impossible
	"""
	properties = [
		Property('domain_name', PropertyTypeString()),
		Property('web_server_ip', PropertyTypeString()),
		Property('users', PropertyTypeList(PropertyTypeEntity(User))),
		Property('inactive_users', PropertyTypeList(PropertyTypeEntity(User)))
	]

class DomainRDPService(Entity):
	"""
	Properties:
	- domain_name - [mandatory] Domain name. 
	- web_server_ip - [mandatory] IP address of the Windows web server.
	- users - [mandatory] Users to check the authentication as.
	- inactive_users - [optional] Users to check that authentication is
	  impossible
	- runner - [optional] Object of the class 'ServerRunnerBase' which can
	  work with the target Windows web node
	"""
	properties = [
		Property('domain_name', PropertyTypeString()),
		Property('web_server_ip', PropertyTypeString()),
		Property('users', PropertyTypeList(PropertyTypeEntity(User))),
		Property('inactive_users', PropertyTypeList(PropertyTypeEntity(User))),
		Property('runner', PropertyType())
	]

class MySQLDatabase(Entity):
	"""
	Properties:
	- source_access - [mandatory] Access data for the database on the source (Plesk/H-Sphere/...) server. Type: MySQLDatabaseAccess.
	- target_access - [mandatory] Access data for the database on the target server. Type: MySQLDatabaseAccess.
	- name - [mandatory] Database name
	- users - [mandatory] Users to check the authentication as
	"""
	properties = [
		Property('source_access', PropertyType()),
		Property('target_access', PropertyType()),
		Property('name', PropertyTypeString()),
		Property('users', PropertyTypeList(PropertyTypeEntity(User)))
	]

class TargetPanelDatabase(Entity):
	"""
	Properties:
	- name - [mandatory] name of a database that exists on the target panel
	- users - [mandatory] list of database users on the target panel related to
	  that database
	"""
	properties = [
		Property('name', PropertyTypeString()),
		Property('users', PropertyTypeList(PropertyTypeString())),
	]

class DomainMySQLDatabases(Entity):
	"""
	Properties:
	- domain_name - [mandatory] Domain name. 
	- databases - [mandatory] List of MySQL databases of domain. Type: list of
	  MySQLDatabase.
	- target_panel_databases - [optional] List of databases that exist of the
	  target panel. If None is passed (or this property is not specified),
	  check of panel databases and users is skipped. Empty list ('[]')
	  should be passed if there are no databases found on target panel.
	"""
	properties = [
		Property('domain_name', PropertyTypeString()),
		Property(
			'databases', 
			PropertyTypeList(PropertyTypeEntity(MySQLDatabase))
		),
		Property(
			'target_panel_databases', 
			PropertyTypeList(PropertyTypeEntity(TargetPanelDatabase))
		)
	]

class MySQLDatabaseAccess(Entity):
	"""
	Properties:
	- mysql_client_cli - [mandatory] Instance of MySQLClientCLI able to execute
	  MySQL command on database server.
	- host - [mandatory] Database host
	- port - [mandatory] Database port
	- admin_user - [mandatory] Admin user information
	"""
	properties = [
		Property('mysql_client_cli', PropertyType()),
		Property('host', PropertyTypeString()), 
		Property('port', PropertyTypeString()), 
		Property('admin_user', PropertyTypeEntity(User)) 
	]

class MySQLClientCLI(object):
	def sh(self, cmd_str, args):
		"""Execute MySQL client binary with specified arguments on some server 

		This interface provides abstraction over:
		- different locations of MySQL client binary, 
		- transport (for example, it could be implemented as SSH transport,
		  Winexe transport, local execution, etc)
		- OS-specific shell escaping rules

		Arguments:
		- cmd_str - MySQL client's command string. There you could
		specify arguments you want to pass to MySQL client
		- args - dictionary of arguments to substitute in command string

		Returns:
		- stdout returned by MySQL client

		Raises: 
		- NonZeroExitCodeException if exit code of executed MySQL client is
		  not 0

		Example of usage: 
		to execute simple query "SELECT login FROM users" with MySQL client,
		you should specify:
		- cmd_str:
			"-h {host} -P {port} -u {user} -p{password} {database} -e {query}"
		- args:
			dict(
				host='localhost', 
				port='3306', 
				password='123qwe', 
				database='exampledb', 
				query='SELECT login FROM users'
			)
		"""
		raise NotImplementedError()

	def connect(self):
		"""Connect to server with the MySQL client binary
		
		Implement there connect to underlying transport.
		"""
		pass

	def disconnect(self):
		"""Disconnect from server with the MySQL client binary
		
		Implement there closing connection to underlying transport.
		"""
		pass

	def use_skip_secure_auth(self):
		"""Whether to pass --skip-secure-auth flag to MySQL client

		This flag is necessary when client version is greater than server version.
		Do not pass it when it is not necessary, or when client is of the same version as server.
		"""
		return False

class DomainMSSQLDatabases(Entity):
	"""
	Properties:
	- domain_name - [mandatory] Domain name. Type: unicode string.
	- databases - [mandatory] List of MSSQL databases of domain. Type: list of
	  MSSQLDatabase.
	- target_panel_databases - [optional] List of databases that exist of the
	  target panel. If None is passed (or this property is not specified),
	  check of panel databases and users is skipped. Empty list ('[]')
	  should be passed if there are no databases found on target panel.
	"""
	properties = [
		Property('domain_name', PropertyTypeString()), 
		Property('databases', PropertyTypeList(PropertyType())),
		Property(
			'target_panel_databases', 
			PropertyTypeList(PropertyTypeEntity(TargetPanelDatabase))
		)
	]

class MSSQLDatabase(Entity): 
	"""
	Properties:
	- source_access - [mandatory] Access data for the database on the source
	  (Plesk/H-Sphere/...) server. Type: MSSQLDatabaseSourceAccess.
	- target_access - [mandatory] Access data for the database on the target
	  server. Type: MSSQLDatabaseTargetAccess.
	- script_runner - [mandatory] Access data for the server to run check
	  scripts. Must be able to connect to both database servers. Type:
		  MSSQLDatabaseScriptRunner
	- name - [mandatory] Database name
	- users - [mandatory] Users to check the authentication as. Type: list of
	  User items.
	"""
	properties = [
		Property('source_access', PropertyType()), 
		Property('target_access', PropertyType()),
		Property('script_runner', PropertyType()),
		Property('name', PropertyTypeString()),
		Property('users', PropertyTypeList(PropertyTypeEntity(User))),
	]

class MSSQLDatabaseSourceAccess(Entity):
	"""
	Properties:
	- host - [mandatory] MSSQL database host
	- admin_user - [mandatory] Admin user information. Type: User.
	"""
	properties = [
		Property('host', PropertyTypeString()), 
		Property('admin_user', PropertyTypeString())
	]

class MSSQLDatabaseTargetAccess(Entity):
	"""
	Properties:
	- host - [mandatory] MSSQL database host
	- admin_user - [mandatory] Admin user information. Type: User.
	"""
	properties = [
		Property('host', PropertyTypeString()), 
		Property('admin_user', PropertyTypeString())
	]

class MSSQLDatabaseScriptRunner(Entity):
	"""
	Properties:
	- runner - [mandatory] Function that can create instance of
	  parallels.core.run_command.BaseRunner to work with the target node
	- get_session_file_path - [mandatory] Function that takes one string as
	  input filename, and generates full path to temporary file on the target
	  node
	"""
	properties = [
		Property('runner', PropertyType()), 
		Property('get_session_file_path', PropertyType()), 
	]

class PostgreSQLDatabase(Entity):
	"""
	Properties:
	- source_access - [mandatory] Access data for the database on the source
	  server. Type: PostgreSQLDatabaseAccess.
	- target_access - [mandatory] Access data for the database on the target
	  server. Type: PostgreSQLDatabaseAccess.
	- name - [mandatory] Database name
	- users - [mandatory] Users to check the authentication as
	"""
	properties = [
		Property('source_access', PropertyType()),
		Property('target_access', PropertyType()),
		Property('name', PropertyTypeString()),
		Property('users', PropertyTypeList(PropertyTypeEntity(User)))
	]

class DomainPostgreSQLDatabases(Entity):
	"""
	Properties:
	- domain_name - [mandatory] Domain name. 
	- databases - [mandatory] List of PostgreSQL databases of domain. Type: list of
	  PostgreSQLDatabase.
	- target_panel_databases - [optional] List of databases that exist of the
	  target panel. If None is passed (or this property is not specified),
	  check of panel databases and users is skipped. Empty list ('[]')
	  should be passed if there are no databases found on target panel.
	"""
	properties = [
		Property('domain_name', PropertyTypeString()),
		Property(
			'databases', 
			PropertyTypeList(PropertyTypeEntity(PostgreSQLDatabase))
		),
		Property(
			'target_panel_databases', 
			PropertyTypeList(PropertyTypeEntity(TargetPanelDatabase))
		)
	]

class PostgreSQLDatabaseAccess(Entity):
	"""
	Properties:
	- runner - [mandatory] an object of the class 'ServerRunnerBase' which
	  provides access to a server where we can execute PostgreSQL CLI client 
	  and get access to source/target database server
	- host - [mandatory] Database host
	- port - [mandatory] Database port
	- admin_user - [mandatory] Admin user information
	"""
	properties = [
		Property('runner', PropertyType()),
		Property('host', PropertyTypeString()), 
		Property('port', PropertyTypeString()), 
		Property('admin_user', PropertyTypeEntity(User)) 
	]

# ========================================================
# Concrete implementation of the checker 
# ========================================================

class DomainServicesHostingChecker(DomainServicesHostingCheckerInterface):
	"""Implementation class for DomainServicesHostingCheckerInterface"""
	def check_website_availability(self, domains_to_check, website_availability_check_timeout=30):
		from parallels.hosting_check.checkers.website_availability_checker import WebsiteAvailabilityChecker
		checker = WebsiteAvailabilityChecker(website_availability_check_timeout)
		return checker.check(domains_to_check)

	def check_dns(
		self, domains_to_check, 
		max_difference_lines=3, save_external_report_data_function=None, 
		dns_server_name='PPA DNS server',
		difference_file_panel_id='ppa'
	):
		from parallels.hosting_check.checkers.dns_checker import DNSChecker
		checker = DNSChecker(
			max_difference_lines, save_external_report_data_function, 
			dns_server_name=dns_server_name,
			difference_file_panel_id=difference_file_panel_id
		)
		return checker.check(domains_to_check)

	def check_mail_auth(self, domains_to_check, messages_delta_limit=5):
		from parallels.hosting_check.checkers.mail_auth_checker import MailAuthChecker
		checker = MailAuthChecker(messages_delta_limit)
		return checker.check(domains_to_check)

	def check_ftp_auth(self, domains_to_check):
		from parallels.hosting_check.checkers.ftp_auth_checker import FTPAuthChecker 
		checker = FTPAuthChecker()
		return checker.check(domains_to_check)
# ========================================================
# Server's interface
# ========================================================

class ServerRunnerBase(object):
	"""An abstract class, that provides access to server shell."""

	def sh(self, command, args):
		"""
		Run a shell command on a server and check exit code.

		This function executes a given command and returns command stdout.
		Function should check command exit code, and if it is not 0, it should
		raise an exception.

		This is a default implementation, that is a wrapper of sh_unchecked().
		Most likely you need to implement 'sh_unchecked' function, 
		but don't need to override 'sh' function.

		Arguments:
			command: a string with a shell command, or a template string with
				variables specified in curly braces, for example:
					'cp {file1} {file2}'
			args: a dictionary of template variables to be substituted in
				'command' string, for example:
					{'file1': '/tmp/file1', 'file2': '/home/me/file2'}
			
		Returns: 
			A string, that contains stdout of the executed command.
		Raises: 
			NonZeroExitCodeException if exit code of executed command is
			not 0
		"""
		exit_code, stdout, stderr = self.sh_unchecked(command, args)
		if exit_code != 0:
				raise NonZeroExitCodeException(exit_code, stdout, stderr, command)
		return stdout

	def sh_unchecked(self, command, args):
		"""
		Run a shell command on a server.

		This function executes a given command and returns command exit code, 
		stdout and stderr.

		Arguments:
			command: a string with a shell command, or a template string with
				variables specified in curly braces, for example:
					'cp {file1} {file2}'
			args: a dictionary of template variables to be substituted in
				'command' string, for example:
					{'file1': '/tmp/file1', 'file2': '/home/me/file2'}
			
		Returns: 
			A tuple (exit_code, stdout, stderr)
		"""
		raise NotImplementedError()

	def upload_file_content(self, filename, content):
		"""
		Upload specified string into file on a server

		Arguments:
		- filename - name of a file on a remote or local server
		where this function will upload file contents to
		- content - string with content to upload
		"""
		raise NotImplementedError()

	def connect(self):
		"""Open server connection."""
		raise NotImplementedError()

	def disconnect(self):
		"""Close server connection."""
		raise NotImplementedError()

class NonZeroExitCodeException(Exception):
	def __init__(self, exit_code, stdout, stderr, command=''):
		self.exit_code = exit_code
		self.stdout = stdout
		self.stderr = stderr
		self.command = command

	def __str__(self):
		return "Non-zero exit code '%s' returned by the command '%s'." % (
					self.exit_code, self.command)
