"""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 contextlib import contextmanager

from parallels.core.utils.common import default
from parallels.core.utils.entity import Entity
from parallels.hosting_check.utils.issue_types import IssueIdsSet

# ========================================================
# 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(object):
    """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(object):
    """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(object):
    """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(object):
    """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(object):
    """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(object):
    """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(object):
    # 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(object):
    # 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(object):
    # 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(object):
    # 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(object):
    # 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(object):
    """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(object):
    """Protocols to check web service on"""

    HTTP = 'http'
    HTTPS = 'https'


class Service(Entity):
    """
    Properties:
    - host - [mandatory] Service host ip
    - description - [mandatory] Service host description
    - server - [mandatory] Object of the class 'ServerBase' representing server
    on which we should check the services
    - service - [mandatory] Object of 'ServiceInfo' which describes a
    checked service
    """
    def __init__(self, host, description, server, service):
        self._host = host
        self._description = description
        self._server = server
        self._service = service

    @property
    def host(self):
        """
        :rtype: str | unicode
        """
        return self._host

    @property
    def description(self):
        """
        :rtype: str | unicode
        """
        return self._description

    @property
    def server(self):
        """
        :rtype: parallels.hosting_check.ServerBase
        """
        return self._server

    @property
    def service(self):
        """
        :rtype: parallels.hosting_check.ServiceInfo
        """
        return self._service


class ProcessInfo(Entity):
    """
    Properties:
    - names - [mandatory] - List names of process which run by service
    - ports - [mandatory]  - List of ports which used by service
    """

    def __init__(self, names, ports):
        self._names = names
        self._ports = ports

    @property
    def names(self):
        """
        :rtype: list[str | unicode]
        """
        return self._names

    @property
    def ports(self):
        """
        :rtype: list[str | unicode]
        """
        return self._ports


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
    """
    def __init__(self, service_type, processes, is_windows, check_connection):
        self._service_type = service_type
        self._processes = processes
        self._is_windows = is_windows
        self._check_connection = check_connection

    @property
    def service_type(self):
        """
        :rtype: str | unicode
        """
        return self._service_type

    @property
    def processes(self):
        """
        :rtype: list[parallels.hosting_check.ProcessInfo]
        """
        return self._processes

    @property
    def is_windows(self):
        """
        :rtype: str | unicode
        """
        return self._is_windows

    @property
    def check_connection(self):
        """
        :rtype: str | unicode
        """
        return self._check_connection


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'.
    - relative_url
    - source_domain_name
    - 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
    - source_protocol
    - source_relative_url
    - 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.
    - server - [optional] an object of the class 'ServerBase' 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
    """
    def __init__(
        self, domain_name, web_server_ip, protocol, relative_url='',
            source_domain_name=None, source_web_server_ip=None, source_protocol=None, source_relative_url=None,
            signatures=None, aps_domain_relative_urls=None, server=None, error_logs=None, www_alias_enabled=False
    ):
        self._domain_name = domain_name
        self._web_server_ip = web_server_ip
        self._protocol = protocol
        self._relative_url = relative_url
        self._source_domain_name = source_domain_name
        self._source_web_server_ip = source_web_server_ip
        self._source_protocol = source_protocol
        self._source_relative_url = source_relative_url
        self._signatures = default(signatures, WEB_SIGNATURES)
        self._aps_domain_relative_urls = default(aps_domain_relative_urls, [])
        self._server = server
        self._error_logs = default(error_logs, [])
        self._www_alias_enabled = www_alias_enabled

    @property
    def domain_name(self):
        """
        :rtype: str | unicode
        """
        return self._domain_name

    @property
    def web_server_ip(self):
        """
        :rtype: str | unicode
        """
        return self._web_server_ip

    @property
    def protocol(self):
        """
        :rtype: str | unicode
        """
        return self._protocol

    @property
    def relative_url(self):
        """
        :rtype: str | unicode
        """
        return self._relative_url

    @property
    def source_domain_name(self):
        """
        :rtype: str | unicode | None
        """
        return self._source_domain_name

    @property
    def source_web_server_ip(self):
        """
        :rtype: str | unicode | None
        """
        return self._source_web_server_ip

    @property
    def source_protocol(self):
        """
        :rtype: str | unicode
        """
        return self._source_protocol

    @property
    def source_relative_url(self):
        """
        :rtype: str | unicode | None
        """
        return self._source_relative_url

    @property
    def signatures(self):
        """
        :rtype: list[parallels.hosting_check.signatures.Signature]
        """
        return self._signatures

    @property
    def aps_domain_relative_urls(self):
        """
        :rtype: list[str | unicode]
        """
        return self._aps_domain_relative_urls

    @property
    def server(self):
        return self._server

    @property
    def error_logs(self):
        """
        :rtype: list[str | unicode]
        """
        return self._error_logs

    @property
    def www_alias_enabled(self):
        """
        :rtype: bool
        """
        return self._www_alias_enabled

    @property
    def server_description(self):
        """
        :rtype: str | unicode
        """
        if self.web_server_ip is not None:
            return 'target server (IP address %s)' % self.web_server_ip
        return 'target server'

    @property
    def source_server_description(self):
        """
        :rtype: str | unicode
        """
        if self.source_web_server_ip is not None:
            return 'source server (IP address %s)' % self.source_web_server_ip
        return 'source server'


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.
    """
    def __init__(self, rec_type, src, dst, opt):
        self._rec_type = rec_type
        self._src = src
        self._dst = dst
        self._opt = opt

    @property
    def rec_type(self):
        """
        :rtype: str | unicode
        """
        return self._rec_type

    @property
    def src(self):
        """
        :rtype: str | unicode
        """
        return self._src

    @property
    def dst(self):
        """
        :rtype: str | unicode
        """
        return self._dst

    @property
    def opt(self):
        """
        :rtype: str | unicode
        """
        return self._opt


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.
    """
    def __init__(self, domain_name, dns_records, dns_servers):
        self._domain_name = domain_name
        self._dns_records = dns_records
        self._dns_servers = dns_servers

    @property
    def domain_name(self):
        """
        :rtype: str | unicode
        """
        return self._domain_name

    @property
    def dns_records(self):
        """
        :rtype: list[parallels.hosting_check.DNSRecord]
        """
        return self._dns_records

    @property
    def dns_servers(self):
        """
        :rtype: list[str | unicode]
        """
        return self._dns_servers


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.
    """
    def __init__(self, domain_name, dns_records, source_dns_servers, target_dns_server):
        self._domain_name = domain_name
        self._dns_records = dns_records
        self._source_dns_servers = source_dns_servers
        self._target_dns_server = target_dns_server

    @property
    def domain_name(self):
        """
        :rtype: str | unicode
        """
        return self._domain_name

    @property
    def dns_records(self):
        """
        :rtype: list[parallels.hosting_check.DNSRecord]
        """
        return self._dns_records

    @property
    def source_dns_servers(self):
        """
        :rtype: list[str | unicode]
        """
        return self._source_dns_servers

    @property
    def target_dns_server(self):
        """
        :rtype: list[str | unicode]
        """
        return self._target_dns_server


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.
    """
    def __init__(self, domain_name, dns_records, source_dns_servers, target_dns_servers):
        self._domain_name = domain_name
        self._dns_records = dns_records
        self._source_dns_servers = source_dns_servers
        self._target_dns_servers = target_dns_servers

    @property
    def domain_name(self):
        """
        :rtype: str | unicode
        """
        return self._domain_name

    @property
    def dns_records(self):
        """
        :rtype: list[parallels.hosting_check.DNSRecord]
        """
        return self._dns_records

    @property
    def source_dns_servers(self):
        """
        :rtype: list[str | unicode]
        """
        return self._source_dns_servers

    @property
    def target_dns_servers(self):
        """
        :rtype: list[str | unicode]
        """
        return self._target_dns_servers


class User(Entity):
    """
    Properties:
    - login - [mandatory] Login. Do not include the domain name here.
    - password - [mandatory] Password. 
    """
    def __init__(self, login, password, password_type=None):
        self._login = login
        self._password = password
        self._password_type = password_type

    @property
    def login(self):
        """
        :rtype: str | unicode
        """
        return self._login

    @property
    def password(self):
        """
        :rtype: str | unicode
        """
        return self._password

    @property
    def password_type(self):
        """
        :rtype: str | unicode | None
        """
        return self._password_type


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.
    """
    def __init__(
        self, domain_name, mail_server_ip, source_mail_server_ip=None, users=None,
        source_users=None, target_panel_mail_users=None
    ):
        self._domain_name = domain_name
        self._mail_server_ip = mail_server_ip
        self._source_mail_server_ip = source_mail_server_ip
        self._users = default(users, [])
        self._source_users = source_users
        self._target_panel_mail_users = target_panel_mail_users

    @property
    def domain_name(self):
        """
        :rtype: str | unicode
        """
        return self._domain_name

    @property
    def mail_server_ip(self):
        """
        :rtype: str | unicode
        """
        return self._mail_server_ip

    @property
    def source_mail_server_ip(self):
        """
        :rtype: str | unicode
        """
        return self._source_mail_server_ip

    @property
    def users(self):
        """
        :rtype: list[parallels.hosting_check.User]
        """
        return self._users

    @property
    def source_users(self):
        """
        :rtype: list[parallels.hosting_check.User] | None
        """
        return self._source_users

    @property
    def target_panel_mail_users(self):
        """
        :rtype: list[str | unicode] | None
        """
        return self._target_panel_mail_users


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
    """
    def __init__(self, domain_name, web_server_ip, users, inactive_users=None):
        self._domain_name = domain_name
        self._web_server_ip = web_server_ip
        self._users = users
        self._inactive_users = default(inactive_users, [])

    @property
    def domain_name(self):
        """
        :rtype: str | unicode
        """
        return self._domain_name

    @property
    def web_server_ip(self):
        """
        :rtype: str | unicode
        """
        return self._web_server_ip

    @property
    def users(self):
        """
        :rtype: list[parallels.hosting_check.User]
        """
        return self._users

    @property
    def inactive_users(self):
        """
        :rtype: list[parallels.hosting_check.User]
        """
        return self._inactive_users


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
    """
    def __init__(self, domain_name, web_server_ip, users, inactive_users=None):
        self._domain_name = domain_name
        self._web_server_ip = web_server_ip
        self._users = users
        self._inactive_users = inactive_users

    @property
    def domain_name(self):
        """
        :rtype: str | unicode
        """
        return self._domain_name

    @property
    def web_server_ip(self):
        """
        :rtype: str | unicode
        """
        return self._web_server_ip

    @property
    def users(self):
        """
        :rtype: list[parallels.hosting_check.User]
        """
        return self._users

    @property
    def inactive_users(self):
        """
        :rtype: list[parallels.hosting_check.User] | None
        """
        return self._inactive_users


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
    - server - [optional] Object of the class 'ServerBase' which corresponds to Windows server
    on which these system users are located
    """
    def __init__(self, domain_name, web_server_ip, users, inactive_users=None, server=None):
        self._domain_name = domain_name
        self._web_server_ip = web_server_ip
        self._users = users
        self._inactive_users = inactive_users
        self._server = server

    @property
    def domain_name(self):
        """
        :rtype: str | unicode
        """
        return self._domain_name

    @property
    def web_server_ip(self):
        """
        :rtype: str | unicode
        """
        return self._web_server_ip

    @property
    def users(self):
        """
        :rtype: list[parallels.hosting_check.User]
        """
        return self._users

    @property
    def inactive_users(self):
        """
        :rtype: list[parallels.hosting_check.User] | None
        """
        return self._inactive_users

    @property
    def server(self):
        return self._server


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.
    - source_name - [mandatory] Database name on the source server
    - target_name - [mandatory] Database name on the target server
    - users - [mandatory] Users to check the authentication as
    """
    def __init__(self, source_access, target_access, source_name, target_name, users):
        self._source_access = source_access
        self._target_access = target_access
        self._source_name = source_name
        self._target_name = target_name
        self._users = users

    @property
    def source_access(self):
        return self._source_access

    @property
    def target_access(self):
        return self._target_access

    @property
    def source_name(self):
        """
        :rtype: str | unicode
        """
        return self._source_name

    @property
    def target_name(self):
        """
        :rtype: str | unicode
        """
        return self._target_name

    @property
    def users(self):
        """
        :rtype: list[parallels.hosting_check.User]
        """
        return self._users


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
    """
    def __init__(self, name, users):
        self._name = name
        self._users = users

    @property
    def name(self):
        """
        :rtype: str | unicode
        """
        return self._name

    @property
    def users(self):
        """
        :rtype: list[str | unicode]
        """
        return self._users


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.
    """
    def __init__(self, domain_name, databases, target_panel_databases=None):
        self._domain_name = domain_name
        self._databases = databases
        self._target_panel_databases = target_panel_databases

    @property
    def domain_name(self):
        """
        :rtype: str | unicode
        """
        return self._domain_name

    @property
    def databases(self):
        """
        :rtype: list[parallels.hosting_check.MySQLDatabase]
        """
        return self._databases

    @property
    def target_panel_databases(self):
        """
        :rtype: list[parallels.hosting_check.TargetPanelDatabase] | None
        """
        return self._target_panel_databases


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
    """
    def __init__(self, mysql_client_cli, host, port, admin_user):
        self._mysql_client_cli = mysql_client_cli
        self._host = host
        self._port = port
        self._admin_user = admin_user

    @property
    def mysql_client_cli(self):
        """
        :rtype: parallels.hosting_check.MySQLClientCLI 
        """
        return self._mysql_client_cli

    @property
    def host(self):
        """
        :rtype: str | unicode
        """
        return self._host

    @property
    def port(self):
        """
        :rtype: str | unicode
        """
        return self._port

    @property
    def admin_user(self):
        """
        :rtype: parallels.hosting_check.User
        """
        return self._admin_user


class MySQLClientCLI(object):
    def execute_mysql_command(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 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.
    """
    def __init__(self, domain_name, databases, target_panel_databases=None):
        self._domain_name = domain_name
        self._databases = databases
        self._target_panel_databases = target_panel_databases

    @property
    def domain_name(self):
        """
        :rtype: str | unicode
        """
        return self._domain_name

    @property
    def databases(self):
        """
        :rtype: list[parallels.hosting_check.MSSQLDatabase]
        """
        return self._databases

    @property
    def target_panel_databases(self):
        """
        :rtype: list[parallels.hosting_check.TargetPanelDatabase] | None
        """
        return self._target_panel_databases


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.
    - source_server - [mandatory] Access data for the source panel server to run SQL queries to check.
    Must be able to connect to source database server.
    - target_server - [mandatory] Access data for the target panel server to run SQL queries to check.
    Must be able to connect to target database server.
    - source_name - [mandatory] Database name on the source server
    - target_name - [mandatory] Database name on the target server
    - users - [mandatory] Users to check the authentication as. Type: list of
    User items.
    """
    def __init__(
        self, source_access, target_access, source_server, target_server, source_name, target_name, users
    ):
        self._source_access = source_access
        self._target_access = target_access
        self._source_server = source_server
        self._target_server = target_server
        self._source_name = source_name
        self._target_name = target_name
        self._users = users

    @property
    def source_access(self):
        return self._source_access

    @property
    def target_access(self):
        return self._target_access

    @property
    def source_server(self):
        """
        :rtype: parallels.hosting_check.ServerBase 
        """
        return self._source_server

    @property
    def target_server(self):
        """
        :rtype: parallels.hosting_check.ServerBase
        """
        return self._target_server

    @property
    def source_name(self):
        """
        :rtype: str | unicode
        """
        return self._source_name

    @property
    def target_name(self):
        """
        :rtype: str | unicode
        """
        return self._target_name

    @property
    def users(self):
        """
        :rtype: list[parallels.hosting_check.User]
        """
        return self._users


class MSSQLDatabaseSourceAccess(Entity):
    """
    Properties:
    - host - [mandatory] MSSQL database host
    - admin_user - [mandatory] Admin user information. Type: User.
    """
    def __init__(self, host, admin_user):
        """
        :type host: str | unicode
        :type admin_user: parallels.hosting_check.User
        """
        self._host = host
        self._admin_user = admin_user

    @property
    def host(self):
        """
        :rtype: str | unicode
        """
        return self._host

    @property
    def admin_user(self):
        """
        :rtype: parallels.hosting_check.User
        """
        return self._admin_user


class MSSQLDatabaseTargetAccess(Entity):
    """
    Properties:
    - host - [mandatory] MSSQL database host
    - admin_user - [mandatory] Admin user information. Type: User.
    """
    def __init__(self, host, admin_user):
        self._host = host
        self._admin_user = admin_user

    @property
    def host(self):
        """
        :rtype: str | unicode
        """
        return self._host

    @property
    def admin_user(self):
        """
        :rtype: str | unicode
        """
        return self._admin_user


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.
    - source_name - [mandatory] Database name on the source server
    - target_name - [mandatory] Database name on the target server
    - users - [mandatory] Users to check the authentication as
    """
    def __init__(self, source_access, target_access, source_name, target_name, users):
        self._source_access = source_access
        self._target_access = target_access
        self._source_name = source_name
        self._target_name = target_name
        self._users = users

    @property
    def source_access(self):
        """
        :rtype: parallels.hosting_check.PostgreSQLDatabaseAccess
        """
        return self._source_access

    @property
    def target_access(self):
        """
        :rtype: parallels.hosting_check.PostgreSQLDatabaseAccess
        """
        return self._target_access

    @property
    def source_name(self):
        """
        :rtype: str | unicode
        """
        return self._source_name

    @property
    def target_name(self):
        """
        :rtype: str | unicode
        """
        return self._target_name

    @property
    def users(self):
        """
        :rtype: list[parallels.hosting_check.User]
        """
        return self._users


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.
    """
    def __init__(self, domain_name, databases, target_panel_databases=None):
        self._domain_name = domain_name
        self._databases = databases
        self._target_panel_databases = target_panel_databases

    @property
    def domain_name(self):
        """
        :rtype: str | unicode
        """
        return self._domain_name

    @property
    def databases(self):
        """
        :rtype: list[parallels.hosting_check.PostgreSQLDatabase]
        """
        return self._databases

    @property
    def target_panel_databases(self):
        """
        :rtype: list[parallels.hosting_check.TargetPanelDatabase] | None
        """
        return self._target_panel_databases


class PostgreSQLDatabaseAccess(Entity):
    """
    Properties:
    - server - [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
    """
    def __init__(self, server, host, port, admin_user):
        self._server = server
        self._host = host
        self._port = port
        self._admin_user = admin_user

    @property
    def server(self):
        """
        :rtype: parallels.hosting_check.ServerBase
        """
        return self._server

    @property
    def host(self):
        """
        :rtype: str | unicode
        """
        return self._host

    @property
    def port(self):
        """
        :rtype: str | unicode
        """
        return self._port

    @property
    def admin_user(self):
        """
        :rtype: parallels.hosting_check.User
        """
        return self._admin_user


# ========================================================
# Server's interface
# ========================================================


class ServerBase(object):
    """An abstract class for server, with access to shell and description"""

    def description(self):
        """Get description of the server

        :rtype: str | unicode
        """
        raise NotImplementedError()

    @contextmanager
    def runner(self):
        """Get runner of the server for shell access, already connected.

        Should yield instance of parallels.core.runners.base.BaseRunner.
        """
        raise NotImplementedError()

    @property
    def get_session_file_path(self):
        """Get path of a directory where we could put temporary files
        
        :rtype: str | unicode
        """
        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)
