import errno
import os
import os.path
import sys
import time
from collections import defaultdict, namedtuple
from contextlib import contextmanager

from parallels.core.logging import log_context

try:
    from ConfigParser import RawConfigParser, MissingSectionHeaderError
except ImportError:
    from configparser import RawConfigParser, MissingSectionHeaderError

from parallels.core import MigrationError, get_logger
from parallels.core import MigrationNoContextError
from parallels.core import messages
from parallels.core.actions.backup.pack import Pack as ActionPackBackups
from parallels.core.actions.backup.unpack import Unpack as ActionUnpackBackups
from parallels.core.actions.migration_list.utils import get_migration_list_file_location
from parallels.core.connections.database_servers.external import ExternalDatabaseServer
from parallels.core.connections.migrator_server import MigratorServer
from parallels.core.converter.business_objects.common import SOURCE_TARGET
from parallels.core.converter.business_objects.plans import convert_hosting_plans
from parallels.core.converter.business_objects.resellers import ResellersConverter
from parallels.core.dump import dump
from parallels.core.global_context import GlobalMigrationContext
from parallels.core.migrated_subscription import MigratedSubscription
from parallels.core.migration_list.entities.plans import MigrationListPlans
from parallels.core.migration_list.factory import create_migration_list_reader
from parallels.core.migration_list.reader.plain_reader import MigrationListReaderPlain
from parallels.core.migration_list.source_data import MigrationListSourceData
from parallels.core.migration_list.writer.plain_writer import MigrationListWriterPlain
from parallels.core.migrator_config import MailContent, read_mssql_copy_method
from parallels.core.panels import load_target_panel_class
from parallels.core.registry import Registry
from parallels.core.reports.model.issue import Issue
from parallels.core.reports.model.report import Report
from parallels.core.reports.multi_report_writer import MultiReportWriter
from parallels.core.reports.persistent_report_writer import PersistentReportWriter
from parallels.core.reports.plain_report import PlainReport
from parallels.core.reports.printer import print_report
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ExecutionOptions
from parallels.core.safe import Safe
from parallels.core.session_files import CommonSessionFiles
from parallels.core.statistics import StatisticsReporter
from parallels.core.utils import database_utils
from parallels.core.utils import steps_profiler
from parallels.core.utils import type_checker
from parallels.core.utils.cache import CacheStateControllers
from parallels.core.utils.common import find_only, unused, safe_format, safe_string_repr
from parallels.core.utils.common import ilen, group_by, if_not_none, cached
from parallels.core.utils.common import open_unicode_file
from parallels.core.utils.common_constants import DATABASE_NO_SOURCE_HOST, LOCK_MIGRATION
from parallels.core.utils.config_utils import ConfigSection
from parallels.core.utils.database_server_type import DatabaseServerType
from parallels.core.utils.database_utils import DatabaseInfo, LinuxDatabaseTransferOptions, db_type_pretty_name
from parallels.core.utils.local_temp_filename import LocalTempFilename
from parallels.core.utils.locks.global_file_lock import GlobalFileLock
from parallels.core.utils.migration_progress import MigrationProgress
from parallels.core.utils.migrator_utils import get_optional_option_value
from parallels.core.utils.mssql_servers_without_physical_access import MSSQLServersWithoutPhysicalAccess
from parallels.core.utils.name_conflicts import PersistentNameMapping
from parallels.core.utils.panel_detector.source import SourcePanelDetector
from parallels.core.utils.panel_detector.target import TargetPanelDetector
from parallels.core.utils.password_holder import PasswordHolder
from parallels.core.utils.rsync_pool import RsyncPool
from parallels.core.utils.session_subscription_report import SessionSubscriptionsReport
from parallels.core.utils.session_subscriptions_status import SessionSubscriptionsStatus
from parallels.core.utils.ssh_key_pool import SSHKeyPool
from parallels.core.utils.stop_mark import StopMark
from parallels.core.utils.user_ssh_access_pool import UserSSHAccessPool
from parallels.core.utils.yaml_utils import write_yaml, read_yaml
from parallels.core.workflow.default_workflow_builder import DefaultWorkflowBuilder
from parallels.core.workflow.runner import create_action_runner
from parallels.plesk.source.plesk.infrastructure_checks import checks as infrastructure_checks
from parallels.plesk.source.plesk.infrastructure_checks.checks import NodesPair, check_mysql_and_pgsql_connections
from parallels.plesk.source.plesk.infrastructure_checks.lister import \
    InfrastructureCheckListerDataSource, InfrastructureCheckLister
from plesk_migrator.compatibility.utils import stringio


class LicenseValidationError(MigrationError):
    pass


MailServer = namedtuple('MailServer', ('ip', 'ssh_auth'))

logger = get_logger(__name__)


class Migrator(object):
    def __init__(self, config, options):
        logger.info(messages.LOG_INITIALIZE_MIGRATOR)

        registry = Registry.get_instance()

        registry.set_migration_lock(GlobalFileLock(LOCK_MIGRATION))

        self._changed_security_policy_nodes = None

        # create context
        self.global_context = registry.set_context(self._create_global_context())
        self.global_context.config = config
        self.global_context.options = options
        self.global_context.start_time = time.time()
        self.global_context.migrator = self
        self.global_context.migrator_server = self._get_migrator_server()
        self.global_context.local_temp_filename = LocalTempFilename()
        self.global_context.progress = MigrationProgress()
        self.global_context.statistics_reporter = StatisticsReporter()

        # create reports and store it in context
        self.global_context.pre_check_report = Report(messages.REPORT_POTENTIAL_ISSUES, None)
        self.global_context.convert_report = Report(messages.REPORT_DETECTED_PROBLEMS, None)
        self.global_context.dns_forwarding_report = Report(messages.REPORT_DNS_FORWARDING_ISSUES, None)
        self.global_context.dns_forwarding_report_writer = MultiReportWriter(
            self.global_context.dns_forwarding_report
        )
        self.global_context.application_adjust_report = Report(messages.REPORT_ADJUSTED_APPLICATIONS, None)
        self.global_context.post_migration_check_report = Report(messages.TRANSFERRED_DOMAINS_FUNCTIONAL_ISSUES, None)
        self.global_context.post_migration_check_report_writer = MultiReportWriter(
            self.global_context.post_migration_check_report
        )
        # When running 'transfer-accounts' or any other command with post-migration checks,
        # we still want to see all post-migration issues as 'test-all' command issues, not as
        # 'transfer-account' command issues.
        self.global_context.post_migration_check_report_writer.set_override_execution_command('test-all')
        self.global_context.test_services_report = Report(messages.REPORT_SERVICE_ISSUES, None)
        self.global_context.execution_migration_report = Report(messages.REPORT_MIGRATION_STATUS, None)
        self.global_context.migration_report_writer = MultiReportWriter(
            self.global_context.execution_migration_report
        )

        # provide useful migration methods via context
        self.global_context.load_raw_dump = self.load_raw_dump
        self.global_context.load_converted_dump = self.load_converted_dump
        self.global_context.load_shallow_dump = self.load_shallow_dump
        self.global_context.iter_windows_servers = self.iter_windows_servers
        self.global_context.iter_all_subscriptions = self.iter_all_subscriptions
        self.global_context.iter_all_subscriptions_unfiltered = self.iter_all_subscriptions_unfiltered
        self.global_context.get_rsync = self._get_rsync
        self.global_context.stop_mark = StopMark()

        if options.is_configuration_required:
            # configure connections and assets, store them in context
            source_panel_info = SourcePanelDetector().detect(self.global_context)
            self.global_context.source_panel_type = source_panel_info.panel_type
            self.global_context.source_panel_dir = source_panel_info.panel_dir
            self.global_context.target_panel_type = TargetPanelDetector().detect(self.global_context)
            self.global_context.conn = self._load_connections_configuration(
                self.global_context, self.global_context.target_panel_type
            )
            target_panel = self._get_target_panel()
            self.global_context.target_panel_obj = target_panel
            self.global_context.hosting_repository = target_panel.get_hosting_repository(self.global_context)
            self.global_context.sources = self.global_context.conn.get_sources()
            self.global_context.source_servers = self.global_context.conn.get_source_servers()

            # create sessions files and corresponding assets, store it in context
            session_files = self._create_session_files()
            self.global_context.session_files = session_files
            self.global_context.password_holder = PasswordHolder(session_files.get_path_to_generated_passwords())
            self.global_context.ssh_key_pool = SSHKeyPool(session_files.get_path_to_ssh_keys_pool())
            self.global_context.user_ssh_access_pool = UserSSHAccessPool()
            self.global_context.rsync_pool = RsyncPool(session_files.get_path_to_rsync())
            self.global_context.progress.set_report_path(session_files.get_path_to_progress())
            self.global_context.name_mapping = PersistentNameMapping(session_files.get_path_to_name_mapping())
            self.global_context.subscriptions_status = SessionSubscriptionsStatus(
                session_files.get_path_to_subscriptions_status()
            )
            self.global_context.subscriptions_report = SessionSubscriptionsReport(
                session_files.get_path_to_subscriptions_report()
            )
            self.global_context.mssql_servers_without_physical_access = MSSQLServersWithoutPhysicalAccess(
                session_files.get_path_to_list_of_mssql_servers_with_no_physical_access()
            )

            # create persistent report for that session
            self.global_context.persistent_migration_report = PersistentReportWriter(
                Report(messages.REPORT_MIGRATION_STATUS, None),
                self.global_context.session_files.get_path_to_migration_report()
            )
            self.global_context.migration_report_writer.append_report(
                self.global_context.persistent_migration_report
            )
            self.global_context.post_migration_check_report_writer.append_report(
                self.global_context.persistent_migration_report
            )
            self.global_context.dns_forwarding_report_writer.append_report(
                self.global_context.persistent_migration_report
            )

            # read target model and provide safe object
            target_model = self._load_target_model(session_files.get_path_to_target_model())
            self.global_context.target_model = target_model
            self.global_context.safe = Safe(target_model)
            self.global_context.cache_state_controllers = CacheStateControllers()

            enable_profiling = get_optional_option_value(options, 'enable_profiling', None)
            if enable_profiling is None:
                # if options '--disable-profiling' or '--enable-profiling' were not specified
                # then check if profiling is enabled by default for command (Command object)
                enable_profiling = options.is_enable_profiling_by_default
            if enable_profiling:
                self._configure_profiler()

        self.action_runner = create_action_runner()

        self.workflow = registry.set_workflow(DefaultWorkflowBuilder().create_workflow(self.global_context))

    def _get_rsync(self, source_server, target_server, source_ip=None):
        """
        Retrieve rsync connection object, that provide ability to transfer files
        between source and target Windows servers
        :type source_server: parallels.core.connections.source_server.SourceServer
        :type target_server: parallels.core.connections.target_servers.TargetServer
        :type source_ip: str
        :rtype: parallels.core.windows_rsync.RsyncControl
        """
        raise NotImplementedError()

    def _configure_profiler(self):
        steps_profiler.configure_report_locations(
            steps_profiler.get_default_steps_report(),
            self._get_migrator_server(),
            Registry.get_instance().get_command_name()
        )

    def configure_type_checker(self):
        type_checker.get_default_type_checker().add_decorator_to_functions()
        type_checker.configure_report_locations(
            type_checker.get_default_type_report(),
            self._get_migrator_server()
        )

    def _create_global_context(self):
        return GlobalMigrationContext()

    def _create_session_files(self):
        """
        Create session files object
        :rtype: parallels.core.session_files.CommonSessionFiles
        """
        return CommonSessionFiles(self.global_context.conn, self._get_migrator_server())

    def _get_source_servers(self):
        return self.global_context.source_servers

    def _check_connections(self, options):
        self._check_target()
        self._check_sources()

    @property
    def web_files(self):
        """Object to list files to be transferred from source to target

        :rtype: parallels.core.utils.paths.web_files.BaseWebFiles
        """
        raise NotImplementedError()

    @property
    def databases_to_copy(self):
        return self._list_databases_to_copy()

    def get_domain_source_web_ip(self, subscription, domain_name):
        """Get IP address of a server from which we should copy web content

        Domain name is name of some domain within specified subscription.
        Override in child classes if domain within specified subscription
        could have another IP than subscription itself.

        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        :type domain_name: str | unicode
        :rtype: str | unicode
        """
        return self.global_context.source_servers[subscription.model.source].ip

    def get_domain_source_web_server(self, subscription, domain_name):
        """Get server object for source web server of specified domain

        Domain name is name of some domain within specified subscription.
        Override in child classes if domain within specified subscription
        could be located on another server than subscription itself.

        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        :type domain_name: str | unicode
        :rtype: parallels.core.connections.source_server.SourceServer
        """
        return subscription.web_source_server

    @staticmethod
    def is_dns_migration_enabled():
        """Whether DNS migration for that source panel is enabled or not.

        By default returns True, override if DNS migration is not supported for that source panel.

        :rtype: bool
        """
        return True

    @staticmethod
    @cached
    def _get_target_panel_by_name(target_panel_name):
        panel_class = load_target_panel_class(target_panel_name, 'panel', 'Panel')
        return panel_class()

    def _get_target_panel(self):
        """Retrieve target panel for migration

        :rtype: parallels.core.target_panel_base.TargetPanelBase
        """
        return self._get_target_panel_by_name(self.global_context.target_panel_type)

    @cached
    def _get_migrator_server(self):
        return MigratorServer(self.global_context.config)

    def _load_connections_configuration(self, global_context, target_panel_type):
        raise NotImplementedError()

    @cached
    def _get_subscription_nodes(self, subscription_name):
        target_panel = self.global_context.target_panel_obj
        return target_panel.get_subscription_nodes(self.global_context, subscription_name)

    def fetch_source(self, options):
        self._check_connections(options)
        self.action_runner.run(
            self.workflow.get_shared_action('fetch-source'), 'fetch-source'
        )

    def _fetch_source(self, options, overwrite):
        self.action_runner.run(
            self.workflow.get_shared_action('fetch-source'), 'fetch-source'
        )

    def _check_target(self):
        try:
            logger.debug(messages.CHECK_CONNECTIONS_TARGET_NODES)
            self.global_context.conn.target.check_connections()
            logger.debug(messages.CHECK_PLESK_VERSION_MAIN_TARGET_NODE)
            self.global_context.target_panel_obj.check_version(self.global_context)
            if not self.global_context.conn.target.is_windows:
                logger.debug(messages.CHECK_LOG_PRIORITY_TARGET_PLESK_NODE)
                self._check_plesk_log_priority()
        except MigrationNoContextError:
            raise
        except Exception as e:
            logger.exception()
            raise MigrationError(messages.ERROR_WHILE_CHECKING_TARGET_PANEL_CONFIGURATION % e)

    def _check_plesk_log_priority(self):
        """Check what log priority of Plesk Panel is 0, because other values can hang Plesk Migrator
        """
        global_section = ConfigSection(self.global_context.config, 'GLOBAL')
        if global_section.getboolean('skip-log-priority-check', False) is True:
            logger.debug(messages.SKIP_CHECKING_LOG_PRIORITY_OPTION)
            return

        target_server = self.global_context.conn.target.plesk_server
        with target_server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            plesk_root = target_server.plesk_dir
            panel_ini_filename = plesk_root + '/admin/conf/panel.ini'
            cat_results = runner.execute_command(
                'cat {filename}', dict(filename=panel_ini_filename),
                ExecutionOptions(ignore_exit_code=True)
            )
            if cat_results.exit_code != 0:
                logger.debug(messages.UNABLE_TO_CAT_FILE % panel_ini_filename)
                return

            panel_ini = cat_results.stdout

            config = RawConfigParser()
            try:
                config.readfp(stringio(panel_ini, is_unicode=False))
            except MissingSectionHeaderError:
                config.readfp(stringio("[root]\n" + panel_ini, is_unicode=False))

            def _check_option(section_name, option_name):
                priority = 0
                if config.has_section(section_name) and config.has_option(section_name, option_name):
                    priority = config.getint(section_name, option_name)
                if priority != 0:
                    h_section_name = 'global' if section_name == 'root' else '[%s]' % section_name
                    raise MigrationNoContextError(
                        messages.DISABLE_HIGH_LOG_PRIORITY.format(
                            server_name=target_server.description(),
                            option_name=option_name,
                            section_name=h_section_name,
                            file_name=panel_ini_filename
                        )
                    )

            _check_option('log', 'priority')
            _check_option('log', 'filter.priority')
            _check_option('root', 'log.priority')
            _check_option('root', 'filter.priority')

    def _check_sources(self):
        self.global_context.conn.check_source_servers()

    def read_migration_list_lazy(self):
        """
        :rtype: parallels.core.migration_list.entities.list_data.MigrationListData
        """
        if self.global_context.migration_list_data is None:
            try:
                self.global_context.migration_list_data = self._read_migration_list()
            except UnicodeDecodeError as e:
                logger.debug(messages.UNABLE_READ_MIGRATION_LIST_FILE, exc_info=e)
                raise MigrationError(
                    messages.MIGRATION_LIST_NON_UNICODE_CHARS)
            # clear cache of previously loaded raw dump
            self.load_raw_dump.clear()
            for source in self.global_context.get_sources_info():
                source.load_raw_dump.clear()

        if self.global_context.migration_list_data is None:
            # when no migration list was specified, generate it and use full contents of generated list
            writer = MigrationListWriterPlain(self.get_migration_list_source_data())
            initial_mapping_text = writer.generate_initial_content(
                existing_subscription_canonical_names=[], target_service_plans={}
            )
            reader = MigrationListReaderPlain(self.get_migration_list_source_data())
            self.global_context.migration_list_data, errors = reader.read(
                stringio(initial_mapping_text, is_unicode=False)
            )

            if len(errors) > 0:
                raise MigrationError(messages.MIGRATION_TOOL_NOT_ENOUGH_INFORMATION_TRANSFER)
        self.global_context.safe.set_plain_report(
            PlainReport(self.global_context.migration_report_writer, self.global_context.migration_list_data)
        )
        return self.global_context.migration_list_data

    def get_migration_list_source_data(self):
        return MigrationListSourceData(lambda: self.iter_shallow_dumps())

    def _read_migration_list(self):
        def reader_func(fileobj):
            reader = create_migration_list_reader(
                self.global_context.options.migration_list_format, self.get_migration_list_source_data()
            )
            subscriptions_only = self.global_context.target_panel_obj.has_subscriptions_only(
                self.global_context
            )
            read_resellers = self.global_context.target_panel_obj.has_resellers(
                self.global_context
            )
            return reader.read(
                fileobj, subscriptions_only, read_resellers
            )

        return self._read_migration_list_data(reader_func)

    def _read_migration_list_data(self, reader_func):
        migration_list_file = get_migration_list_file_location(self.global_context)

        if migration_list_file is None or not os.path.exists(migration_list_file):
            return None

        try:
            with open_unicode_file(migration_list_file) as fileobj:
                mapping, errors = reader_func(fileobj)
        except (IOError, OSError) as e:
            logger.exception()
            raise MigrationError(
                messages.FAILED_TO_READ_MIGRATION_LIST % (migration_list_file, safe_string_repr(e))
            )

        if len(errors) > 0:
            error_message = messages.ERRORS_READING_MIGRATION_LIST.format(errors="\n".join(errors))
            if not self.global_context.options.ignore_migration_list_errors:
                raise MigrationError(error_message)
            else:
                logger.warning(error_message)
                self.global_context.safe.fail_general(error_message, '', Issue.SEVERITY_WARNING)

        return mapping

    def _get_safe_lazy(self):
        return self.global_context.safe

    def get_target_model(self, apply_filtering=True):
        """Get target model from context and apply filtering for repeatability feature: objects (subscriptions,
        clients, resellers, plans) that have some problems should be skipped on all the further steps
        :type apply_filtering: bool
        """
        if apply_filtering:
            safe = self._get_safe_lazy()
            return safe.get_filtering_model()
        else:
            return self.global_context.target_model

    def _convert_model(self, root_report, target_existing_objects):
        reseller_plans = self.global_context.target_panel_obj.get_reseller_plan_converter_adapter().convert(
            self.global_context,
            self._read_migration_list_resellers(),
            root_report
        )
        hosting_plans = convert_hosting_plans(
            self.global_context,
            self._read_migration_list_plans(),
            root_report
        )
        migration_list_data = self.read_migration_list_lazy()
        resellers = self._convert_resellers(
            hosting_plans,
            target_existing_objects.resellers,
            if_not_none(migration_list_data, lambda m: m.resellers),
            root_report
        )
        target_model = self._convert_accounts(reseller_plans, hosting_plans, resellers, root_report)
        self._write_target_model(target_model)
        return target_model

    def _load_target_model(self, path_to_target_model):
        if not os.path.exists(path_to_target_model):
            return None
        return self._read_target_model(path_to_target_model)

    @staticmethod
    def _read_target_model(path_to_target_model):
        """Load target model from given file
        :type path_to_target_model: str
        """
        try:
            return read_yaml(path_to_target_model)
        except IOError as err:
            if err.errno == errno.ENOENT:
                raise MigrationError(messages.TARGET_MODEL_FILE_NOT_FOUND)
            else:
                raise

    def _write_target_model(self, target_model):
        """Store target model into:
        - a file, so other steps that are executed within another migrator execution could use the model
        - memory, so other steps that are executed within the same migration execution,
        could use it without loading model from file
        """
        target_model_filename = self.global_context.session_files.get_path_to_target_model()
        write_yaml(target_model_filename, target_model)
        logger.debug(messages.SAVED_TARGET_MODEL_TO_FILE.format(filename=target_model_filename))
        self.global_context.target_model = target_model
        self.global_context.safe.model = target_model

    def _load_reseller_mapping(self, model):
        reseller_logins = [reseller.login for reseller in model.resellers.values()]
        return dict(
            (reseller.username, reseller.reseller_id)
            for reseller in self.global_context.hosting_repository.reseller.get_list(reseller_logins)
        )

    def _load_client_mapping(self, model):
        filter_username = [customer.login for customer in model.iter_all_customers()]
        return dict(
            (customer.username, customer.customer_id)
            for customer in self.global_context.hosting_repository.customer.get_list(filter_username=filter_username)
        )

    def convert_hosting(self, options):
        self._check_connections(options)
        self.read_migration_list_lazy()
        self._convert_hosting()

    def _convert_hosting(self):
        logger.info(messages.LOG_CONVERT_HOSTING_SETTINGS)
        self.action_runner.run(self.workflow.get_shared_action(
            'convert-hosting'
        ))

    def restore_hosting(self, options):
        self.read_migration_list_lazy()
        self._check_connections(options)
        self._restore_hosting_impl()

    def _restore_hosting_impl(self):
        self.action_runner.run(self.workflow.get_path(
            'transfer-accounts/restore-hosting'
        ))

    def _fix_iis_idn_401_error(self):
        """ After migration we have following issue: IIS (actual on Windows 2008, seems that was fixed
        for Windows 2008 R2 and Windows 2012) time-to-time returns 401 error code for http and
        https requests instead of 200. Known workaround is a execution of iisreset command after hosting restoration.
        """
        nodes_for_iis_reset = []

        def is_idn_domain(domain_name):
            return domain_name != domain_name.encode('idna')

        subscriptions_by_source = self._get_all_windows_subscriptions_by_source()

        for server_id, raw_dump in self.iter_panel_server_raw_dumps():
            with log_context(server_id):
                for subscription_name in subscriptions_by_source[server_id]:
                    for site_name in [subscription_name] + [
                        site.name for site in raw_dump.iter_sites(subscription_name)
                    ]:
                        if is_idn_domain(site_name):
                            subscription_web_node = self._get_subscription_nodes(subscription_name).web
                            if subscription_web_node not in nodes_for_iis_reset:
                                nodes_for_iis_reset.append(subscription_web_node)

        for node in nodes_for_iis_reset:
            logger.debug(messages.RESTART_IIS_FOR_IDN_DOMAINS, node.description())
            with node.runner() as runner:
                assert isinstance(runner, BaseRunner)
                runner.execute_command('iisreset')

    def restore_status(self, options):
        self.read_migration_list_lazy()

        model = self.get_target_model()

        safe = self._get_safe_lazy()

        def suspend_clients(reseller_login, clients):
            for client in clients:
                with safe.try_client(reseller_login, client.login, messages.FAILED_TO_SUSPEND_CUSTOMER):
                    if client.source != SOURCE_TARGET:
                        # clients that are already on target should not be touched,
                        # however their subscriptions that are not on target should be
                        if not client.is_enabled:
                            logger.debug(messages.DEBUG_SUSPEND_CUSTOMER)
                            self.global_context.hosting_repository.customer.disable_by_username(client.login)

        def suspend_resellers(resellers):
            for reseller in resellers:
                with safe.try_reseller(reseller.login, messages.FAILED_TO_SUSPEND_RESELLER):
                    # reseller itself should not be suspended as we migrate only stubs for resellers: contact data only,
                    # reseller is not assigned to any subscription by migrator,
                    # so there is nothing to suspend, and we suspend only clients of a reseller
                    suspend_clients(reseller.login, reseller.clients)

        suspend_clients(None, model.clients.values())
        suspend_resellers(model.resellers.values())

    def _get_subscription_first_dns_ip(self, subscription_name, domain_name):
        """Returns IP address of the first DNS server on target

        If there are no DNS servers related to the subscription - exception is raised
        """
        subscription = self._create_migrated_subscription(subscription_name)
        dns_server_ips = subscription.target_dns_ips
        if len(dns_server_ips) == 0:
            raise Exception(messages.AT_LEAST_ONE_DNS_SERVER_SHOULD % domain_name)
        return dns_server_ips[0]

    def _convert_resellers(self, hosting_plans, existing_resellers, resellers_migration_list, report):
        converter = ResellersConverter(self.global_context.target_panel_obj.is_encrypted_passwords_supported())
        return converter.convert_resellers(
            self.global_context.get_primary_sources_info(),
            hosting_plans,
            existing_resellers,
            resellers_migration_list,
            report,
            self.global_context.password_holder
        )

    def _convert_accounts(self, reseller_plans, hosting_plans, resellers, report):
        converter_adapter = self.global_context.target_panel_obj.get_converter_adapter()
        return converter_adapter.convert(self.global_context, reseller_plans, hosting_plans, resellers, report)

    def _fetch_resellers_from_target(self, reseller_logins):
        return dict(
            (reseller.username, reseller)
            for reseller in self.global_context.hosting_repository.reseller.get_list(reseller_logins)
        )

    def _read_migration_list_resellers(self):
        panel = self._get_target_panel()
        if not panel.has_subscriptions_only(self.global_context) and panel.has_resellers(self.global_context):
            reader = create_migration_list_reader(
                self.global_context.options.migration_list_format, self.get_migration_list_source_data()
            )
            return self._read_migration_list_data(
                lambda fileobj: reader.read_resellers(fileobj)
            )
        else:
            return {}

    def _read_migration_list_plans(self):
        panel = self._get_target_panel()
        if not panel.has_subscriptions_only(self.global_context):
            reader = create_migration_list_reader(
                self.global_context.options.migration_list_format, self.get_migration_list_source_data()
            )
            read_resellers = self.global_context.target_panel_obj.has_resellers(self.global_context)
            return self._read_migration_list_data(
                lambda fileobj: reader.read_plans(fileobj, read_resellers)
            )
        else:
            return MigrationListPlans()

    @staticmethod
    def _print_dns_forwarding_report(report):
        if report.has_issues():
            print_report(report, "dns_forwarding_setup_report_tree")
            sys.exit(1)
        else:
            logger.info(messages.DNS_FORWARDING_WAS_SUCCESSFULLY_SET_UP)

    @staticmethod
    def _print_dns_forwarding_undo_report(report):
        if report.has_issues():
            print_report(report, "dns_forwarding_undo_report_tree")
            sys.exit(1)
        else:
            logger.info(messages.DNS_FORWARDING_WAS_SUCCESSFULLY_UNDONE)

    def transfer_wpb_sites(self, options, finalize=False):
        raise NotImplementedError()

    def unpack_backups(self):
        ActionUnpackBackups().run(self.global_context)

    def pack_backups(self):
        ActionPackBackups().run(self.global_context)

    def iter_all_subscriptions(self):
        target_model = self.get_target_model()
        for model_subscription in target_model.iter_all_subscriptions():
            yield self._create_migrated_subscription(model_subscription.name)

    def iter_all_subscriptions_unfiltered(self):
        target_model = self.get_target_model(apply_filtering=False)
        for model_subscription in target_model.iter_all_subscriptions():
            yield self._create_migrated_subscription(model_subscription.name)

    @cached
    def _create_migrated_subscription(self, name):
        """Return instance of MigrationSubscription"""
        return MigratedSubscription(self, name)

    def _is_fake_domain(self, subscription_name):
        """Check if domain is fake - created by technical reasons

        Fake domains may be not existent on source server. Many operations,
        like copy content, perform post migration checks, etc should not be
        performed, or should be performed in another way for such domain.

        By default, there are no fake domains.
        Overridden in Helm 3 migrator which has fake domains.
        """
        return False

    def _get_all_subscriptions_by_source(self, is_windows=None):
        target_model = self.get_target_model()
        subscriptions_by_source = defaultdict(list)
        for subscription in target_model.iter_all_subscriptions():
            if is_windows is None or subscription.is_windows == is_windows:
                subscriptions_by_source[subscription.source].append(subscription.name)
        return subscriptions_by_source

    def _get_all_unix_subscriptions_by_source(self):
        return self._get_all_subscriptions_by_source(is_windows=False)

    def _get_all_windows_subscriptions_by_source(self):
        return self._get_all_subscriptions_by_source(is_windows=True)

    def transfer_subscription_databases(self, subscription):
        databases_by_subscription = group_by(
            self._list_databases_to_copy(log_assimilated_databases=True),
            lambda database_info: database_info.subscription_name
        )
        subscription_databases = databases_by_subscription.get(subscription.name, [])

        for db_info in subscription_databases:
            self._copy_database_content(db_info)

    def _copy_database_content(self, db_info):
        """
        :type db_info: parallels.core.migrator.DatabaseInfo
        """
        safe = self._get_safe_lazy()

        if not self._check_database_exists_on_target(db_info):
            return

        if db_info.source_database_server.is_windows():
            if not db_info.target_database_server.is_windows():
                safe.fail_subscription(
                    db_info.subscription_name,
                    # XXX can't check this condition on 'check' step, but the error issued here is kinda too late
                    messages.FAILED_COPY_CONTENT_DATABASE_S_MIGRATION % db_info.source_database_name,
                    solution=messages.FIX_SERVICE_TEMPLATE_SUBSCRIPTION_IS_ASSIGNED,
                    is_critical=False
                )
            else:
                repeat_error = messages.UNABLE_COPY_CONTENT_DATABASE_S_SUBSCRIPTION % (
                    db_info.source_database_name, db_info.subscription_name
                )

                def _repeat_copy_windows_content():
                    mssql_copy_method = read_mssql_copy_method(self.global_context.config)
                    database_utils.copy_db_content_windows(db_info, mssql_copy_method)

                safe.try_subscription_with_rerun(
                    _repeat_copy_windows_content,
                    db_info.subscription_name,
                    messages.FAILED_TO_COPY_DATABASE_CONTENT % db_info.source_database_name,
                    is_critical=False,
                    repeat_error=repeat_error,
                    use_log_context=False
                )
        else:
            if db_info.target_database_server.is_windows():
                safe.fail_subscription(
                    db_info.subscription_name,
                    # XXX can't check this condition on 'check' step, but the error issued here is kinda too late
                    messages.FAILED_COPY_DB_CONTENT_UNIX_WINDOWS % db_info.source_database_name,
                    solution=messages.FIX_SERVICE_TEMPLATE_SUBSCRIPTION_IS_ASSIGNED_1,
                    is_critical=False
                )
            else:
                repeat_error = messages.UNABLE_TO_COPY_DATABASE_CONTENT_RETRY % (
                    db_info.source_database_name, db_info.subscription_name
                )

                def _repeat_copy_linux_content():
                    keyfile = self.global_context.ssh_key_pool.get(
                        db_info.source_database_server.utilities_server,
                        db_info.target_database_server.utilities_server
                    ).key_pathname
                    transfer_options = LinuxDatabaseTransferOptions(
                        keyfile=keyfile, dump_format='binary', owner=None)
                    database_utils.copy_db_content_linux(db_info, transfer_options)

                safe.try_subscription_with_rerun(
                    _repeat_copy_linux_content,
                    db_info.subscription_name,
                    messages.FAILED_COPY_DATABASE_CONTENT % db_info.source_database_name,
                    is_critical=False,
                    repeat_error=repeat_error,
                    use_log_context=False
                )

    def _check_database_exists_on_target(self, db_info):
        """Check if database exists on target server, put error in case it does not

        :type db_info: parallels.core.migrator.DatabaseInfo
        :rtype: bool
        """
        safe = self._get_safe_lazy()
        target_databases = self._list_target_server_databases(db_info.target_database_server)

        if target_databases is not None:
            if (
                db_info.target_database_server.type() == DatabaseServerType.MYSQL and
                db_info.target_database_server.is_windows()
            ):
                db_name_comparable = db_info.target_database_name.lower()
                target_databases = {db.lower() for db in target_databases}
            else:
                db_name_comparable = db_info.target_database_name

            if db_name_comparable not in target_databases:
                safe.fail_subscription(
                    db_info.subscription_name,
                    messages.DATABASE_DOES_NOT_EXIST % (
                        db_info.target_database_server.type_description, db_info.target_database_name
                    ),
                    solution=(
                        messages.RESOLVE_ISSUE_DATABASE_CREATION_CREATE_DATABASE)
                )
                return False

            if not self.global_context.hosting_repository.database.is_exists_by_subscription(
                    db_info.subscription_name, db_info.target_database_name
            ):
                safe.fail_subscription(
                    db_info.subscription_name,
                    messages.DATABASE_IS_NOT_REGISTERED_IN_PLESK % (
                        db_info.target_database_server.type_description, db_info.target_database_name
                    ),
                    solution=(
                        messages.RESOLVE_ISSUE_DATABASE_NOT_REGISTERED_IN_PLESK)
                )
                return False

        return True

    @staticmethod
    def _list_target_server_databases(target_database_server):
        """List of databases on target database server

        Necessary not to request target database server multiple times during copy-db-content step
        Can use cache as no new databases are added on that step.

        Returns list of databases names or None if that function is not supported for that database server type.
        In case of any issue when retrieving database list - skip to avoid whole database migration to fail.

        :rtype: set[str | unicode] | None
        """
        try:
            return target_database_server.list_databases()
        except Exception as e:
            # In case of any exception - skip to avoid whole database migration to fail
            logger.exception()
            logger.error(
                messages.FAILED_TO_DETECT_DB_LIST, target_database_server.description(), safe_string_repr(e)
            )
            return None

    def _list_databases_to_copy(self, log_assimilated_databases=False):
        """
        :param bool log_assimilated_databases: whether to print databases that
        will be assimilated to info.log, set that parameter to True when
        actually copying databases, and to False in all other cases -
        infrastructure checks, and so on, not to bother customer with extra messages
        :rtype: list[parallels.core.utils.database_utils.DatabaseInfo]
        """
        databases = []

        for subscription in self.global_context.iter_all_subscriptions():
            with self.global_context.safe.try_subscription(
                subscription.name, messages.FAILED_DETECT_DATABASES_SUBSCRIPTION
            ):
                for database in subscription.databases:
                    database_to_copy = self._get_database_to_copy(subscription, database)
                    if database_to_copy:
                        databases.append(database_to_copy)

        return databases

    def _get_database_to_copy(self, subscription, database, log_assimilated_databases=False):
        """Retrieve information necessary to copy specified database of subscription

        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        :type database: parallels.core.migrated_database.MigratedDatabase
        :type log_assimilated_databases: bool
        :rtype: parallels.core.utils.database_utils.DatabaseInfo | None
        """
        if database.raw_dump.host == DATABASE_NO_SOURCE_HOST:
            return None

        source_database_server_model = self._get_src_db_server(
            database.raw_dump.host, database.raw_dump.port, database.dbtype, subscription.server_raw_dump
        )
        if source_database_server_model is None:
            self.global_context.safe.fail_subscription(
                subscription.name,
                safe_format(
                    messages.ISSUE_DATABASE_SERVER_NOT_FOUND,
                    db_type=db_type_pretty_name(database.dbtype), db_name=database.source_name,
                    db_server_host=database.raw_dump.host
                ),
                is_critical=False
            )
            return None

        target_database_server = self._get_target_database_server(subscription.name, database.dbtype)
        if target_database_server is None:
            self.global_context.safe.fail_subscription(
                subscription.name,
                safe_format(
                    messages.DB_NOT_COPIED_NO_DB_SERVER,
                    db=database.raw_dump, subscr=subscription.name
                ),
                is_critical=False
            )
            return None

        source_database_server = self._get_source_database_server(
            subscription.web_source_server.id, source_database_server_model
        )

        if subscription.is_assimilate_database(database.source_name, database.dbtype):
            log_function = logger.info if log_assimilated_databases else logger.debug
            log_function(messages.DEBUG_DB_ASSIMILATION.format(db=database.raw_dump))
            return None

        return DatabaseInfo(
            source_database_server=source_database_server,
            target_database_server=target_database_server,
            subscription_name=subscription.name,
            source_database_name=database.source_name,
            target_database_name=database.target_name
        )

    def _get_source_database_server(self, source_id, database_server_dump):
        """Retrieve source database server

        :type source_id: str
        :type database_server_dump: parallels.core.dump.data_model.DatabaseServer
        :rtype: parallels.core.connections.database_servers.base.DatabaseServer | None
        """
        source_db_node = self._get_source_db_node(source_id)
        if source_db_node is None:
            return None

        external_db_server = self._get_external_database_server(
            source_db_node,
            database_server_dump.dbtype,
            database_server_dump.host,
            database_server_dump.port,
            database_server_dump.login,
            database_server_dump.password
        )
        if external_db_server is not None:
            return external_db_server
        else:
            return source_db_node.get_database_server(database_server_dump)

    def _get_target_database_server(self, subscription_name, db_type):
        target_db_nodes = self._get_subscription_nodes(subscription_name).database
        target_database_server = target_db_nodes.get(db_type)
        if target_database_server is None:
            return None

        external_db_server = self._get_external_database_server(
            target_database_server.panel_server, db_type, target_database_server.host(),
            target_database_server.port(), target_database_server.user(), target_database_server.password()
        )
        if external_db_server is not None:
            return external_db_server
        else:
            return target_database_server

    def _get_external_database_server(self, panel_server, db_type, host, port, user, password):
        external_server_id = self.global_context.conn.get_external_db_server_id(db_type, host)
        if external_server_id is not None:
            settings = self.global_context.conn.get_external_db_servers()[external_server_id]
            return ExternalDatabaseServer(
                settings=settings,
                panel_server=panel_server,
                port=port, user=user, password=password,
                physical_server=self.global_context.conn.get_external_db_physical_server(external_server_id),
            )
        else:
            return None

    def _get_source_db_node(self, source_id):
        return self.global_context.conn.get_source_node(source_id)

    def _get_src_db_server(self, database_server_host, database_server_port, database_server_type, server_dump):
        # this method is overridden for other panels because in common case we don't know administrator
        # credentials of source database servers and can not dump it from source panel
        source_db_servers = {(dbs.host, dbs.port): dbs for dbs in server_dump.iter_db_servers()}
        return source_db_servers.get((database_server_host, database_server_port))

    def _get_subscription_name_by_domain_name(self, domain_name):
        for subscription in self.iter_all_subscriptions():
            for domain in subscription.converted_dump.iter_domains():
                if domain.name == domain_name:
                    return subscription._name

        raise Exception(
            messages.FAILED_FIND_DOMAIN_IN_CONVERTED_BACKUP % domain_name
        )

    def _refresh_service_node_components_for_windows(self):
        """Refresh components list of Windows target nodes in Plesk database.

        It is necessary when:
        - customer need to migrate sites with old ASP.NET 1.1 support or FrontPage support.
        - customer installed corresponding components on a target Windows server (manually)
        - customer has not updated components list of the service node in Plesk database
        So Plesk restore considers the components are not installed, and does not restore corresponding settings.
        """
        try:
            self.global_context.hosting_repository.panel.refresh_components()
        except Exception as e:
            logger.exception()
            # Just a warning, for most of customers refresh components list operation is not actual
            logger.warning(messages.FAILED_REFRESH_COMPONENTS_LIST_FOR_WINDOWS, e)

    def iter_windows_servers(self):
        """Iterate all target windows servers used by migrated subscriptions

        :rtype: list[parallels.core.connections.target_servers.TargetServer |
        parallels.core.connections.physical_server.PhysicalServer]
        """
        windows_servers = []
        for subscription in self.global_context.iter_all_subscriptions():
            subscription_web_server = self._get_subscription_nodes(subscription.name).web
            if (
                subscription_web_server is None or
                not subscription_web_server.is_windows() or
                subscription_web_server in windows_servers
            ):
                continue
            windows_servers.append(subscription_web_server)
            yield subscription_web_server

            target_database_server = self._get_target_database_server(subscription.name, 'mssql')
            if (
                target_database_server is None or
                target_database_server.physical_server is None or
                target_database_server.physical_server in windows_servers
            ):
                continue
            windows_servers.append(target_database_server.physical_server)
            yield target_database_server.physical_server

    def transfer_vdirs(self, options, finalize=True):
        pass

    @staticmethod
    def _use_psmailbackup():
        return True

    # ======================== infrastructure checks ==========================

    def check_main_node_disk_space_requirements(self, options, show_skip_message=True):
        """Check that main target node meets very basic disk space requirements for migration"""
        if options.skip_main_node_disk_space_checks:
            return

        if self.global_context.conn.target.is_windows:
            # main node disk space requirements check for Windows are not implemented
            # so silently skip it
            return

        check_success = True  # in case of failure - just consider checks were successful
        try:
            logger.info(
                messages.CHECK_DISK_SPACE_REQUIREMENTS_FOR_S,
                self.global_context.conn.target.main_node_description()
            )
            with self.global_context.conn.target.main_node_runner() as runner:
                target_model = self.get_target_model()
                subscriptions_count = ilen(target_model.iter_all_subscriptions())
                check_success = infrastructure_checks.check_main_node_disk_requirements(
                    runner, subscriptions_count
                )
        except Exception as e:
            logger.exception()
            logger.error(
                messages.FAILED_TO_CHECK_DISK_SPACE_REQUIREMENTS,
                self.global_context.conn.target.main_node_description(), e
            )

        if not check_success:
            raise MigrationNoContextError(
                (
                    messages.NOT_ENOUGH_DISK_SPACE +
                    ((u" " + messages.HOW_TO_SKIP_DISK_SPACE_CHECK) if show_skip_message else u"")
                ) % (
                    self.global_context.conn.target.main_node_description(),
                    self.global_context.conn.target.main_node_description()
                )
            )
        else:
            logger.info(
                messages.DISK_SPACE_REQUIREMENTS_FOR_S_ARE,
                self.global_context.conn.target.main_node_description()
            )

    def check_infrastructure(self, options):
        """Check that network services are available and that there is enough disk space.

        There are two kinds of infrastructure checks:

        1) Connections checks: check that all necessary connections are working
        (all nodes involved in migration are on, firewall allows connections, etc)
        so we can copy content.
        2) Disk space checks: check that nodes have enough disk space for
        migrated data and temporary files.
        """
        if options.skip_infrastructure_checks:
            return

        infrastructure_check_success = True  # in case of failure - just consider checks were successful
        try:
            @contextmanager
            def safe(check_report, error_message):
                def report_internal_error(exception):
                    check_report.add_issue(
                        'infrastructure_checks_internal_error', Issue.SEVERITY_WARNING,
                        messages.INTERNAL_ERROR_INFRASTRUCTURE_CHECKS % error_message
                    )
                    logger.error(messages.ERROR_AND_EXCEPTION_MESSAGE, error_message, exception)

                try:
                    yield
                except Exception as error:
                    logger.exception()
                    report_internal_error(error)

            report = Report(messages.REPORT_INFRASTRUCTURE, None)
            with safe(report, messages.FAILED_TO_CHECK_CONNECTIONS):
                self._check_infrastructure_connections(report, safe)
            with safe(report, messages.FAILED_CHECK_DISK_SPACE_REQUIREMENTS):
                self._check_disk_space(report, safe)
            with safe(report, messages.FAILED_CHECK_MYSQL_REQUIREMENTS):
                self._check_mysql_max_allowed_packet(report)

            print_report(report, 'infrastructure_checks_report_tree')

            infrastructure_check_success = not report.has_errors()
        except Exception as e:
            logger.exception()
            logger.error(messages.INFRASTRUCTURE_CHECK_INTERNAL_ERROR, e)

        if not infrastructure_check_success:
            raise MigrationNoContextError(messages.CRITICAL_INFRASTRUCTURE_ISSUES)

    def _check_infrastructure_connections(self, report, safe):
        logger.info(messages.LOG_CHECK_CONNECTION_REQUIREMENTS)
        checks = infrastructure_checks.InfrastructureChecks()

        web_report = report.subtarget(messages.CONNECTIONS_BETWEEN_SOURCE_AND_DESTINATION_WEB, None)
        with safe(web_report, messages.FAILED_CHECK_CONNECTIONS_WEB_SERVERS):
            self._check_unix_copy_web_content_rsync_connections(checks, web_report)
            self._check_windows_copy_web_content_rsync_connections(checks, web_report)

        mail_report = report.subtarget(messages.CONNECTIONS_BETWEEN_SOURCE_AND_DESTINATION_MAIL, None)
        with safe(mail_report, messages.FAILED_CHECK_CONNECTIONS_BETWEEN_MAIL_SERVERS):
            self._check_unix_copy_mail_content_rsync_connections(checks, mail_report)
            self._check_windows_copy_mail_content(checks, mail_report)

        db_report = report.subtarget(messages.CONNECTIONS_BETWEEN_SOURCE_AND_DESTINATION_DATABASE, None)
        with safe(db_report, messages.FAILED_CHECK_CONNECTIONS_DB_SERVERS):
            self._check_unix_copy_db_content_scp_connections(checks, db_report)
            self._check_windows_mysql_copy_db_content_rsync_connections(checks, db_report)
            self._check_windows_copy_mssql_db_content(db_report)
            target_db_servers = list({
                d.target_database_server for d in self._list_databases_to_copy()
                if d.target_database_server.type() in [DatabaseServerType.MYSQL, DatabaseServerType.POSTGRESQL]
            })
            check_mysql_and_pgsql_connections(report, target_db_servers)

    def _check_disk_space(self, report, safe):
        logger.info(messages.LOG_CHECK_DISK_SPACE_REQUIREMENTS)
        disk_space_report = report.subtarget(messages.REPORT_DISK_SPACE_REQUIREMENTS, None)
        self._check_disk_space_unix(disk_space_report)
        self._check_disk_space_windows(disk_space_report)

    def _check_disk_space_unix(self, report):
        """Check disk space requirements for hosting content transfer.

        Generate a mapping from source and target server to subscriptions, then
        walk on that structure and call UnixDiskSpaceChecker.
        """
        # Map structure:
        #   [target_node][source_node]['web'] = list of subscriptions
        #   [target_node][source_node]['mail'] = list of mail domains (subscriptions + addon domains)
        #   [target_node][source_node]['mysql_databases'] = list of MySQL databases
        #          (infrastructure_checks.DiskSpaceDatabase)
        subscriptions_by_nodes = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))

        lister = self._get_infrastructure_check_lister()

        web_subscriptions = lister.get_subscriptions_by_unix_web_nodes()
        for nodes, subscriptions in web_subscriptions.items():
            node_subscriptions = subscriptions_by_nodes[nodes.target][nodes.source]
            node_subscriptions['web'].extend(subscriptions)

        mail_subscriptions = lister.get_subscriptions_by_unix_mail_nodes()
        for nodes, subscriptions in mail_subscriptions.items():
            node_subscriptions = subscriptions_by_nodes[nodes.target][nodes.source]
            node_subscriptions['mail'].extend(subscriptions)

        mysql_databases = lister.get_mysql_databases_info_for_disk_space_checker(
            is_windows=False,
        )
        for nodes, databases in mysql_databases.items():
            node_subscriptions = subscriptions_by_nodes[nodes.target][nodes.source]
            node_subscriptions['mysql_databases'].extend(databases)

        for target_node, subscriptions_by_source in subscriptions_by_nodes.items():
            source_nodes = []
            for source_node, content_info in subscriptions_by_source.items():
                mysql_databases = [
                    db_info for db_info in self._list_databases_to_copy()
                    if (
                        db_info.source_database_server.utilities_server == source_node and
                        db_info.target_database_server.utilities_server == target_node and
                        db_info.source_database_server.type() == 'mysql'
                    )
                ]
                source_node_info = infrastructure_checks.UnixDiskSpaceSourcePleskNode(
                    node=source_node,
                    web_domains=content_info.get('web', []),
                    mail_domains=content_info.get('mail', []),
                    mysql_databases=mysql_databases
                )
                source_nodes.append(source_node_info)

            disk_space_checker = infrastructure_checks.UnixDiskSpaceChecker()
            disk_space_checker.check(
                target_node,
                source_nodes=source_nodes,
                report=report
            )

        mysql_databases_by_source_nodes = defaultdict(list)
        for db_info in self._list_databases_to_copy():
            if (
                not db_info.source_database_server.utilities_server.is_windows() and
                db_info.source_database_server.type() == 'mysql'
            ):
                mysql_databases_by_source_nodes[db_info.source_database_server.utilities_server].append(db_info)

        checker_source = infrastructure_checks.UnixSourceSessionDirDiskSpaceChecker()
        for source_node, mysql_databases in mysql_databases_by_source_nodes.items():
            checker_source.check(source_node, mysql_databases, report)

    def _check_disk_space_windows(self, report):
        lister = self._get_infrastructure_check_lister()

        mysql_databases = lister.get_mysql_databases_info_for_disk_space_checker(
            is_windows=True
        )
        mssql_databases = lister.get_mssql_databases_info_for_disk_space_checker()

        subscriptions_by_nodes = defaultdict(list)
        for nodes, subscriptions in lister.get_subscriptions_by_windows_web_nodes().items():
            subscriptions_by_nodes[nodes.target].append((nodes.source, subscriptions))

        checker = infrastructure_checks.WindowsDiskSpaceChecker()

        for target_node, subscriptions_by_source in subscriptions_by_nodes.items():
            source_nodes = []
            for source_node, subscriptions in subscriptions_by_source:
                source_node_info = infrastructure_checks.WindowsDiskSpaceSourceNode(
                    node=source_node,
                    domains=subscriptions,
                    mysql_databases=mysql_databases.get(NodesPair(source_node, target_node), []),
                    mssql_databases=mssql_databases.get(NodesPair(source_node, target_node), [])
                )
                source_nodes.append(source_node_info)

            checker.check(
                target_node,
                mysql_bin=database_utils.get_windows_mysql_client(target_node),
                source_nodes=source_nodes,
                report=report
            )

        mysql_databases_by_source_nodes = defaultdict(list)
        for db_info in self._list_databases_to_copy():
            if (
                db_info.source_database_server.utilities_server.is_windows() and
                db_info.source_database_server.type() == 'mysql'
            ):
                mysql_databases_by_source_nodes[db_info.source_database_server.utilities_server].append(db_info)

        checker_source = infrastructure_checks.WindowsSourceSessionDirDiskSpaceChecker()
        for source_node, databases in mysql_databases_by_source_nodes.items():
            checker_source.check(source_node, mysql_databases=databases, report=report)

    def _check_windows_copy_mssql_db_content(self, report):
        checker = infrastructure_checks.MSSQLConnectionChecker(
            mssql_copy_method=read_mssql_copy_method(self.global_context.config)
        )
        subscriptions = self._get_infrastructure_check_lister().get_subscriptions_by_mssql_instances().items()
        for servers, databases in subscriptions:
            checker.check(servers, databases, report)

    def _check_windows_copy_mail_content(self, checks, report):
        lister = self._get_infrastructure_check_lister()
        if not self._use_psmailbackup():
            checks.check_windows_mail_source_connection(
                subscriptions_by_source=lister.get_subscriptions_to_check_source_mail_connections(MailContent.FULL),
                report=report,
                checker=infrastructure_checks.SourceNodeImapChecker()
            )
        checks.check_windows_mail_source_connection(
            subscriptions_by_source=lister.get_subscriptions_to_check_source_mail_connections(MailContent.MESSAGES),
            report=report,
            checker=infrastructure_checks.SourceNodePop3Checker()
        )
        checks.check_windows_mail_target_connection(
            subscriptions_by_target=lister.get_subscriptions_to_check_target_imap(),
            report=report,
            checker=infrastructure_checks.TargetNodeImapChecker()
        )

    def _check_unix_copy_content(self, checker, nodes_pair_info):
        """
        Arguments
            checker - instance of UnixFileCopyBetweenNodesChecker
        """
        return checker.check(nodes_pair_info, self.global_context.ssh_key_pool)

    def _check_unix_copy_web_content_rsync_connections(self, checks, report):
        subscriptions = self._get_infrastructure_check_lister().get_subscriptions_by_unix_web_nodes().items()

        checks.check_copy_content(
            check_type='rsync',
            subscriptions=subscriptions,
            report=report,
            check_function=lambda nodes_pair_info: self._check_unix_copy_content(
                checker=infrastructure_checks.UnixFileCopyBetweenNodesRsyncChecker(),
                nodes_pair_info=nodes_pair_info
            ),
            content_type='web'
        )

    def _check_unix_copy_mail_content_rsync_connections(self, checks, report):
        subscriptions = self._get_infrastructure_check_lister().get_subscriptions_by_unix_mail_nodes().items()

        checks.check_copy_content(
            check_type='rsync',
            subscriptions=subscriptions,
            report=report,
            check_function=lambda nodes_pair_info: self._check_unix_copy_content(
                checker=infrastructure_checks.UnixFileCopyBetweenNodesRsyncChecker(),
                nodes_pair_info=nodes_pair_info
            ),
            content_type='mail'
        )

    def _check_unix_copy_db_content_scp_connections(self, checks, report):
        subscriptions = self._get_infrastructure_check_lister().get_subscriptions_by_unix_db_nodes().items()
        checks.check_copy_db_content(
            check_type='scp',
            subscriptions=subscriptions,
            report=report,
            check_function=lambda nodes_pair_info: self._check_unix_copy_content(
                checker=infrastructure_checks.UnixFileCopyBetweenNodesScpChecker(),
                nodes_pair_info=nodes_pair_info
            )
        )

    def _check_windows_copy_web_content_rsync_connections(self, checks, report):
        subscriptions = self._get_infrastructure_check_lister().get_subscriptions_by_windows_web_nodes().items()

        checks.check_copy_content(
            check_type='rsync',
            subscriptions=subscriptions,
            report=report,
            check_function=lambda nodes_pair_info: self._check_windows_copy_content(
                checker=infrastructure_checks.WindowsFileCopyBetweenNodesRsyncChecker(),
                nodes_pair_info=nodes_pair_info
            ),
            content_type='web'
        )

    def _check_windows_mysql_copy_db_content_rsync_connections(self, checks, report):
        subscriptions = self._get_infrastructure_check_lister().get_subscriptions_by_windows_mysql_nodes().items()

        checks.check_copy_db_content(
            check_type='rsync',
            subscriptions=subscriptions,
            report=report,
            check_function=lambda nodes_pair_info: self._check_windows_copy_content(
                checker=infrastructure_checks.WindowsFileCopyBetweenNodesRsyncChecker(),
                nodes_pair_info=nodes_pair_info
            )
        )

    def _check_windows_copy_content(self, checker, nodes_pair_info):
        """
        Arguments
            checker - instance of WindowsFileCopyBetweenNodesRsyncChecker
        """
        return checker.check(nodes_pair_info, self._get_rsync)

    def _check_mysql_max_allowed_packet(self, report):
        logger.info(messages.CHECK_MYSQL_MAX_ALLOWED_PACKET)
        mysql_report = report.subtarget(messages.CHECK_MYSQL_MAX_ALLOWED_PACKET_1, None)
        self._check_mysql_max_allowed_packet_unix(mysql_report)
        self._check_mysql_max_allowed_packet_windows(mysql_report)

    def _check_mysql_max_allowed_packet_unix(self, report):
        source_target_pairs = set([
            (d.source_database_server, d.target_database_server) for d in self._list_databases_to_copy()
            if d.source_database_server.type() == 'mysql' and not d.source_database_server.is_windows()
        ])
        for source_database_server, target_database_server in source_target_pairs:
            mysql_checker = infrastructure_checks.MysqlMaxAllowedPacketChecker()
            mysql_checker.check(
                source_database_server=source_database_server,
                target_database_server=target_database_server,
                report=report
            )

    def _check_mysql_max_allowed_packet_windows(self, report):
        source_target_pairs = set([
            (d.source_database_server, d.target_database_server) for d in self._list_databases_to_copy()
            if d.source_database_server.type() == 'mysql' and d.source_database_server.is_windows()
        ])
        for source_database_server, target_database_server in source_target_pairs:
            mysql_checker = infrastructure_checks.MysqlMaxAllowedPacketChecker()
            mysql_checker.check(
                source_database_server=source_database_server,
                target_database_server=target_database_server,
                report=report
            )

    def _get_infrastructure_check_lister(self):
        class InfrastructureCheckListerDataSourceImpl(InfrastructureCheckListerDataSource):
            """Provide necessary information about subscriptions and nodes to
            InfrastructureCheckLister class, requesting Migrator class for that information"""

            def list_databases_to_copy(_self):
                """Provide list of databases migration tool is going to copy
                (list of parallels.plesk.source.plesk.migrator.DatabaseToCopy)

                :rtype: list[parallels.core.utils.database_utils.DatabaseInfo]
                """
                return self._list_databases_to_copy()

            def get_source_node(_self, source_node_id):
                """Get SourceServer object by source node ID"""
                return self._get_source_node(source_node_id)

            def get_source_mail_node(_self, subscription_name):
                """Get SourceServer object for source mail node
                by main (web hosting) source node ID. Mail node may differ
                from web hosting node in case of Expand Centralized Mail
                for example"""
                return self._create_migrated_subscription(subscription_name).mail_source_server

            def get_target_nodes(_self, subscription_name):
                """Get target nodes (SubscriptionNodes) of subscription"""
                return self._get_subscription_nodes(subscription_name)

            def get_target_model(_self):
                """Get target data model (common.target_data_model.Model)"""
                return self.get_target_model()

            def create_migrated_subscription(_self, subscription_name):
                """Create MigrationSubscription object for subscription"""
                return self._create_migrated_subscription(subscription_name)

        return InfrastructureCheckLister(
            InfrastructureCheckListerDataSourceImpl()
        )

    # ======================== utility functions ==============================

    def get_raw_dump_filename(self, server_id):
        return self.global_context.session_files.get_raw_dump_filename(server_id)

    def get_converted_dump_filename(self, backup_id):
        return self.global_context.session_files.get_converted_dump_filename(backup_id)

    # @cached  # unhashable type: 'SourcePleskUnixConfig' on python3
    def load_shallow_dump(self, config):
        if self.shallow_dump_supported(config.source_id):
            session_files = self.global_context.session_files
            backup_filename = session_files.get_path_to_shallow_plesk_backup(config.source_id)
            if os.path.exists(backup_filename):
                return dump.load(backup_filename)
            else:
                # If shallow backup does not exist - fallback to full backup.
                #
                # That situation is possible when customer created migration
                # list manually (so, shallow backup was not created by
                # 'generate-migration-list' command). But still we have full backup
                # created by 'fetch-source' command. And we don't want to fetch shallow
                # backup (it will take some time - performance), so we use full backup instead.
                return self.load_raw_dump(config)
        else:
            return self.load_raw_dump(config)

    @staticmethod
    def shallow_dump_supported(source_id):
        unused(source_id)
        # By default, use the same "full" dump for all purposes
        return False

    @cached
    def load_raw_dump(self, source_config):
        backup_filename = self.get_raw_dump_filename(source_config.source_id)
        return dump.load(
            backup_filename,
            self.global_context.migration_list_data,
            is_expand_mode=self.is_expand_mode(),
            discard_mailsystem=self._is_mail_centralized(source_config.source_id),
        )

    @cached
    def load_converted_dump(self, backup_id):
        filename = self.get_converted_dump_filename(backup_id)
        return dump.load(
            filename,
            self.global_context.migration_list_data,
            is_expand_mode=self.is_expand_mode(),
            discard_mailsystem=self._is_mail_centralized(backup_id),
        )

    def _iter_source_backups_paths(self):
        """Get paths to backup files to restore hosting settings from.
        Returns iter((node_id, path_to_backup_file, is_provisioning_to_plesk_needed))
        """
        for server_id in self.global_context.source_servers.keys():
            yield server_id, self.get_raw_dump_filename(server_id)

    def _iter_converted_backups_paths(self):
        """Get paths to backup files to restore hosting settings from.
        Returns iter((node_id, path_to_backup_file, is_provisioning_to_plesk_needed))
        """
        for server_id in self.global_context.source_servers.keys():
            yield server_id, self.get_converted_dump_filename(server_id)

    @staticmethod
    def is_expand_mode():
        return False

    def _is_mail_centralized(self, server_id):
        return False

    def _get_source_web_node(self, subscription_name):
        subscription = find_only(
            self.get_target_model().iter_all_subscriptions(),
            lambda s: s.name == subscription_name,
            error_message=messages.FAILED_FIND_SUBSCRIPTION_BY_NAME_1)
        source_node_id = subscription.source
        if not self._has_source_node(source_node_id):
            return None
        return self._get_source_node(source_node_id)

    @cached
    def _has_source_node(self, node_id):
        return node_id in self._get_source_servers()

    @cached
    def _get_source_node(self, node_id):
        source_servers = self._get_source_servers()
        if node_id not in source_servers:
            raise Exception(messages.UNABLE_RETRIEVE_SOURCE_NODE_WITH_ID % node_id)
        return self.global_context.conn.get_source_node(node_id)

    def _get_source_dns_ips(self, source_id):
        return [self.global_context.get_source_info(source_id).ip]

    def _get_subscription_content_ip(self, sub):
        return self.global_context.source_servers[sub.source].ip

    def iter_shallow_dumps(self):
        for source in self.global_context.conn.get_sources():
            shallow_dump = self.load_shallow_dump(source.config)
            yield source.source_id, shallow_dump

    def iter_panel_server_raw_dumps(self):
        """Iterate on the servers specified in the config option 'sources'"""
        for server_id, server_settings in self.global_context.source_servers.items():
            raw_dump = self.load_raw_dump(server_settings)
            yield server_id, raw_dump

    def get_panel_server_raw_dump(self, server_id):
        """Return a raw dump found among the servers set in the option 'sources'."""
        for current_backup_id, raw_dump in self.iter_panel_server_raw_dumps():
            if current_backup_id == server_id:
                return raw_dump
        raise Exception(messages.UNABLE_RETRIEVE_RAW_BACKUP % server_id)

    def iter_all_server_raw_dumps(self):
        """Iterate on the extended set of source servers (source panel-specific)."""
        for server_id, server_settings in self._get_source_servers().items():
            raw_dump = self.load_raw_dump(server_settings)
            yield server_id, raw_dump

    def get_any_server_raw_dump(self, backup_id):
        """Return a raw dump found among a set of servers specific to the source panel type."""
        for current_backup_id, raw_dump in self.iter_all_server_raw_dumps():
            if current_backup_id == backup_id:
                return raw_dump
        raise Exception(messages.UNABLE_RETRIEVE_RAW_BACKUP % backup_id)

    def iter_converted_dumps(self):
        for server_id, server_settings in self.global_context.source_servers.items():
            converted_dump = self.load_converted_dump(server_settings.id)
            yield server_id, converted_dump

    @staticmethod
    def _iter_subscriptions_by_report_tree(backup, server_report):
        for subscription in backup.iter_admin_subscriptions():
            subscription_report = server_report.subtarget(u"Subscription", subscription.name)
            yield subscription, subscription_report
        for client in backup.iter_clients():
            client_report = server_report.subtarget(u"Client", client.login)
            for subscription in client.subscriptions:
                subscription_report = client_report.subtarget(u"Subscription", subscription.name)
                yield subscription, subscription_report
        for reseller in backup.iter_resellers():
            reseller_report = server_report.subtarget(u"Reseller", reseller.login)
            for subscription in reseller.subscriptions:
                subscription_report = reseller_report.subtarget(u"Subscription", subscription.name)
                yield subscription, subscription_report
            for client in reseller.clients:
                client_report = reseller_report.subtarget(u"Client", client.login)
                for subscription in client.subscriptions:
                    subscription_report = client_report.subtarget(u"Subscription", subscription.name)
                    yield subscription, subscription_report
