from __future__ import absolute_import

import logging
from contextlib import closing, contextmanager
import os
import os.path
import stat
import sys
import time
import errno
import re
from urllib2 import urlopen
from collections import defaultdict, namedtuple
import itertools
from ConfigParser import RawConfigParser, MissingSectionHeaderError
from StringIO import StringIO

import parallels.core
from parallels.core import messages
from parallels.core.actions.hosting_settings.check.check_database_conflicts import CheckDatabaseConflicts
from parallels.core.connections.database_server import ExternalDatabaseServer
from parallels.core.converter.business_objects.common import SOURCE_TARGET
from parallels.core.panels import load_source_panel_class, load_target_panel_class
from parallels.core.registry import Registry
from parallels.core.reseller_importer import ResellerImporter
from parallels.core.utils.common_constants import DATABASE_NO_SOURCE_HOST, ADMIN_ID, INSTALLATION_TYPE_STANDALONE
from parallels.core.utils.database_server_type import DatabaseServerType
from parallels.core.utils.database_utils import DatabaseInfo, compare_mssql_servers, LinuxDatabaseTransferOptions 
from parallels.core.utils.json_utils import write_json
from parallels.core.utils.migration_progress import MigrationProgress
from parallels.core.utils.common import obj
from parallels.core.utils.common.ip import resolve_all
from parallels.core.utils.common import \
	ilen, group_by, if_not_none, cached, group_by_id, is_run_on_windows
from parallels.core.connections.connections import Connections
from parallels.core.dump import dump
from parallels.core.migrator_config import MailContent, read_mssql_copy_method
from parallels.core import MigrationNoContextError
from parallels.core.logging_context import log_context, subscription_context
from parallels.core.utils import plesk_api_utils
from parallels.core.utils.plesk_utils import get_admin_guid
from parallels.core.workflow.base_workflow import BaseWorkflow
from parallels.core.workflow.shared_hosting_workflow import SharedHostingWorkflowExtension
from parallels.plesk.utils.xml_rpc.plesk import operator as plesk_ops
from parallels.plesk.utils.xml_rpc.plesk.operator.subscription import Ips
from parallels.core.migration_list import MigrationList
from parallels.core.utils.yaml_utils import write_yaml, read_yaml, pretty_yaml
from . import checking
import parallels.core.utils.restore_hosting_utils
from .existing_objects_model import ExistingObjectsModel
from parallels.core.utils.migrator_utils import trace, get_customer_name_in_report, get_package_extras_file_path
from parallels.core.utils.common import partition_list, get_executable_file_name
from parallels.core.utils.common import open_unicode_file
from parallels.core.utils import steps_profiler
from parallels.core.utils import type_checker
from parallels.core.utils.config_utils import ConfigSection
from parallels.core.utils import migrator_utils, database_utils
from parallels.core.hosting_check.reporting import \
	print_backup_hosting_report, print_service_hosting_report
from parallels.core.utils.common import find_only
from parallels.hosting_check import User
from parallels.hosting_check.checkers.mysql_database_checker import \
	UnixMySQLClientCLI, WindowsMySQLClientCLI
from parallels.core.target_panels import TargetPanels
from parallels.core.migrated_subscription import MigratedSubscription
from parallels.hosting_check import messages as hosting_check_messages
from parallels.core.hosting_check import check_hosting_checks_source
from parallels.core.hosting_check.entity_source.common import \
	ServerBackupsCheckSource
from parallels.core.hosting_check.entity_source.service import \
	ServiceCheckSource
from parallels.core.hosting_check.entity_source.web import \
	HostingObjectWebSubscription
from parallels.core.hosting_check.entity_source.mail import \
	HostingObjectMailSubscription
from parallels.core.hosting_check.entity_source.dns import \
	HostingObjectDNSSubscription
from parallels.core.hosting_check.entity_source.database import \
	HostingObjectDatabaseSubscription, DatabasesInfoSourceInterface
from parallels.core.hosting_check.entity_source.users import \
	HostingObjectSubscriptionWithUsers
from parallels.core.hosting_check.entity_source.compound import \
	CompoundHostingCheckSource
from parallels.core.hosting_check.utils.runner_adapter import \
	HostingCheckerRunnerAdapter
from parallels.core.hosting_check.config import \
	MigratorHostingCheckersConfig
from parallels.core.connections.migrator_server import MigratorServer
from parallels.core.session_files import CommonSessionFiles
from parallels.core.actions.backup.unpack import Unpack as ActionUnpackBackups
from parallels.core.connections.source_server import SourceServer
from parallels.core.global_context import GlobalMigrationContext
from parallels.plesk.source.plesk.infrastructure_checks.checks import NodesPair, check_database_server_connections
from parallels.plesk.source.plesk.infrastructure_checks import checks as infrastructure_checks
from parallels.plesk.source.plesk.infrastructure_checks.lister import \
	InfrastructureCheckListerDataSource, InfrastructureCheckLister
from parallels.core.converter.business_objects.password_holder import PasswordHolder
from parallels.core.converter.business_objects.resellers import ResellersConverter
from parallels.core.workflow.runner import create_action_runner
from parallels.core.utils.ssh_key_pool import SSHKeyPool
from parallels.core.utils.rsync_pool import RsyncPool
from parallels.core.actions.migration_list.utils import get_migration_list_file_location
from . import MigrationError, run_and_check_local_command
from .safe import Safe


class LicenseValidationError(MigrationError):
	pass

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

pretty_yaml(plesk_ops.user.UserContactInfo, prefix='plesk')
pretty_yaml(plesk_ops.user.UserGetInfo, prefix='plesk')
pretty_yaml(plesk_ops.user.UserGenInfo, prefix='plesk')
pretty_yaml(plesk_ops.role.RoleInfo, prefix='plesk')


logger = logging.getLogger(__name__)


class Migrator(object):
	def __init__(self, config):
		super(Migrator, self).__init__()

		logger.debug(messages.MIGRATOR_START % (sys.argv[0], " ".join("'%s'" % arg for arg in sys.argv[1:])))
		logger.info(messages.LOG_INITIALIZE_MIGRATOR)

		registry = Registry.get_instance()

		self.config = config

		self.global_context = registry.set_context(self._create_global_context())
		self.global_context.migrator = self
		self.global_context.config = self.config
		self.global_context.migrator_server = self._get_migrator_server()

		self._load_configuration()

		self.read_migration_list = False
		self.target_model = None
		self.safe = None

		self._changed_security_policy_nodes = None

		self.source_servers = self.conn.get_information_servers()
		self.external_db_servers = self.conn.get_external_db_servers()

		self.session_files = self._create_session_files()

		self.global_context.conn = self.conn
		self.global_context.session_files = self.session_files
		self.global_context.import_api = self._get_target_panel_api()
		self.global_context.safe = self._get_safe_lazy()
		self.global_context.target_panel = self.target_panel
		self.global_context.target_panel_obj = self._get_target_panel()
		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.source_servers = self.source_servers
		self.global_context.iter_all_subscriptions = self._iter_all_subscriptions
		self.global_context.password_holder = PasswordHolder(
			self._get_session_file_path('generated-passwords.yaml')
		)
		self.global_context.ssh_key_pool = SSHKeyPool(
			self.global_context.session_files.get_path_to_ssh_keys_pool()
		)
		self.global_context.rsync_pool = RsyncPool(
			self.global_context.session_files.get_path_to_rsync()
		)
		self.global_context.get_rsync = self._get_rsync

		self.global_context.migration_list_data = None

		self.global_context.pre_check_report = checking.Report(
			messages.REPORT_POTENTIAL_ISSUES, None
		)
		self.global_context.dns_forwarding_report = checking.Report(
			messages.REPORT_DNS_FORWARDING_ISSUES, None
		)

		self.global_context.progress = MigrationProgress()

		self.action_runner = create_action_runner()

		self.workflow = registry.set_workflow(self._create_workflow())

	def _create_workflow(self):
		workflow = BaseWorkflow()
		shared_hosting_extension = SharedHostingWorkflowExtension()
		shared_hosting_extension.extend_workflow(workflow)

		# extend workflow with target-panel specific actions
		target_panel_workflow_class = load_target_panel_class(
			self.target_panel, 'workflow', 'WorkflowExtension'
		)
		target_workflow_extension = target_panel_workflow_class()
		target_workflow_extension.extend_workflow(workflow)

		# extend workflow with source-panel specific actions
		source_workflow_extension_class = load_source_panel_class(
			self.target_panel, self.source_panel, 'workflow', 'WorkflowExtension'
		)
		source_workflow_extension = source_workflow_extension_class()
		source_workflow_extension.extend_workflow(workflow)

		return workflow

	def _get_rsync(self, source_server, target_server, source_ip=None):
		"""
		Retrive 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 set_options(self, options):
		self.global_context.options = options

	def configure_profiler(self):
		steps_profiler.configure_report_locations(
			steps_profiler.get_default_steps_report(),
			self._get_migrator_server()
		)

	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):
		return CommonSessionFiles(self.conn, self._get_migrator_server())

	def _get_source_servers(self):
		return self.source_servers

	def _check_connections(self, options):
		self._check_target()
		self._check_sources()
		if not options.skip_services_checks:
			self._check_target_services()

	@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

	@cached
	def _get_target_panel(self):
		panel_class = load_target_panel_class(self.target_panel, 'panel', 'Panel')
		panel_object = panel_class()
		return panel_object

	@cached
	def _get_target_panel_api(self):
		return self._get_target_panel().get_import_api(self.global_context)

	def _load_configuration(self):
		logger.info(messages.LOG_LOAD_CONFIGURATION)

		self.target_panel = self.config.get('GLOBAL', 'target-type')
		self.source_panel = self.config.get('GLOBAL', 'source-type')
		logger.debug(messages.DEBUG_TARGET_PANEL, self.target_panel)
		logger.debug(messages.DEBUG_SOURCE_PANEL, self.source_panel)

		self.conn = self._load_connections_configuration()

		global_section = ConfigSection(self.config, 'GLOBAL')

		transfer_domains_modes = ['same', 'separate']
		transfer_domains_mode = global_section.get('transfer-domains-to-subscription', "separate").lower()
		if transfer_domains_mode not in transfer_domains_modes:
			raise MigrationError(
				messages.INVALID_VALUE_TRANSFER_DOMAIN_TO_SUBSCRIPTION % (
					transfer_domains_mode, ",".join(transfer_domains_modes)
				)
			)
		self.multiple_webspaces = transfer_domains_mode == 'same'

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

	def _load_connections_configuration(self):
		return Connections(self.global_context, self._get_target_panel())

	def _get_session_file_path(self, filename):
		return self._get_migrator_server().get_session_file_path(filename)

	@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_updates()
		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_updates(self):
		# check for updates on Windows not implemented yet, so skip it
		if is_run_on_windows():
			return

		if migrator_utils.get_installation_type() != INSTALLATION_TYPE_STANDALONE:
			return

		# process internal option for developers/support to skip migration tool updates
		if 'skip-migrator-updates' in self.config.options('GLOBAL') and self.config.getboolean('GLOBAL', 'skip-migrator-updates'):
			logger.info(messages.SKIP_CHECKING_PANEL_MIGRATOR_UPDATES)
			return

		try:
			logger.info(messages.LOG_CHECK_MIGRATOR_UPDATES)
			available_version = urlopen('http://autoinstall.plesk.com/panel-migrator/version').read().strip()
			installed_version = migrator_utils.get_version()
		except Exception as e:
			logger.debug(messages.LOG_EXCEPTION, exc_info=e)
			logger.info(messages.FAILED_TO_CHECK_MIGRATOR_UPDATES)
			return

		if not available_version > installed_version:
			logger.info(messages.LOG_NO_UPDATES)
			return

		logger.info(messages.NEW_VERSION_MIGRATION_TOOLS_ALREADY_EXISTS % (installed_version, available_version))
		try:
			# download installer and override it if exist
			installer_file_content = urlopen('http://autoinstall.plesk.com/panel-migrator/installer.sh').read()
			installer_file_path = self._get_session_file_path('installer.sh')
			with open(installer_file_path, 'w') as installer_file:
				installer_file.write(installer_file_content)

			os.chmod(installer_file_path, stat.S_IEXEC)

			# run installer with --upgrade argument
			run_and_check_local_command([installer_file_path, '--upgrade'])

			logger.info(messages.MIGRATION_TOOLS_WERE_UPDATED_LATEST_VERSION)
			sys.exit(0)
		except Exception:
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)
			logger.info(messages.FAILED_TO_UPDATE_MIGRATOR)

	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.debug(messages.LOG_EXCEPTION, exc_info=True)
			raise MigrationError(messages.ERROR_WHILE_CHECKING_TARGET_PANEL_CONFIGURATION % e)

	@trace('check-target-services', messages.CHECK_SERVICES_TARGET_SERVERS)
	def _check_target_services(self):
		try:
			logger.debug(messages.CHECK_CONNECTIONS_TARGET_NODES_1)
			self.global_context.test_services_report = checking.Report(
				messages.REPORT_SERVICE_ISSUES, None
			)
			checks_source = ServiceCheckSource(
				self._get_target_panel().get_service_nodes(self.conn.target),
				self.conn.target
			)
			util_name = get_executable_file_name()
			check_hosting_checks_source(
				checks_source, self.global_context.test_services_report,
				hosting_check_messages.get_solutions(
					self._get_target_panel().get_hosting_check_messages_panel_id(), util_name
				),
				self._get_hosting_checkers_config()
			)
			self._print_test_services_report()
		except Exception as e:
			logger.debug(messages.LOG_EXCEPTION, exc_info=e)
			logger.error(
				messages.FAILED_CHECK_SERVICES_S_CHECK_DEBUGLOG, e
			)

	def _check_plesk_log_priority(self):
		"""Check log priority of Plesk Panel.

		Log priority should be 0 due to for other values migration tool can hang.
		"""

		if 'skip-log-priority-check' in self.config.options('GLOBAL') and self.config.getboolean('GLOBAL', 'skip-log-priority-check'):
			logger.debug(messages.SKIP_CHECKING_LOG_PRIORITY_OPTION)
			return

		target_server = self.conn.target.plesk_server
		with target_server.runner() as runner:
			plesk_root = target_server.plesk_dir
			file_name = plesk_root + '/admin/conf/panel.ini'
			exit_code, panel_ini, _ = runner.run_unchecked('cat', [file_name])
			if exit_code != 0:
				logger.debug(messages.UNABLE_CAT_S_FILE_MOST_PROBABLY % file_name)
				return

			config = RawConfigParser()
			try:
				config.readfp(StringIO(panel_ini))
			except MissingSectionHeaderError:
				config.readfp(StringIO("[root]\n" + panel_ini))

			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=file_name
						)
					)

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

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

	def _fetch(self, options):
		# Determine list of objects for which we need to retrieve from target panel
		migration_list_data = self._read_migration_list_lazy(options)

		# now that we have - in migration_list_data - the resellers, clients, plans and subscriptions
		# we still need to process the dumps to extract from them all domain_names to be transferred.
		domain_names = []
		for server_id, _ in self._iter_source_backups_paths():
			with closing(self.load_raw_dump(self.source_servers[server_id])) as raw_dump:
				for subscription in raw_dump.iter_all_subscriptions():
					domain_names.append(subscription.name)
					domain_names += [site.name for site in raw_dump.iter_sites(subscription.name)]
					domain_names += [alias.name for alias in raw_dump.iter_aliases(subscription.name)]

		self._fetch_target_existing_objects(
			migration_list_data.resellers, migration_list_data.customers_mapping.keys(),
			migration_list_data.plans, migration_list_data.subscriptions_mapping.keys(), domain_names
		)

	def _fetch_target_existing_objects(self, reseller_logins, client_logins, plans, subscription_names, domain_names):
		"""Fetch information about objects existing on target panel,
		only for the specified resellers, clients, plans, subscriptions and domains.
		@param reseller_logins [ reseller_login ] - fetch resellers with these logins
		@param client_logins [ client_login ] - fetch customers with these logins
		@param plans { reseller_login: [ plan_name ] } - fetch service templates with these owners and names
		@param subscription_names { subscription_name } - fetch target webspaces with these names
		@param domain_names { domain_name } - fetch Plesk subscriptions, sites, subdomains and site
			aliases with these domain names
		"""
		logger.info(messages.GET_TARGET_PANELS_RESELLERS_FOR_CONFLICT)
		target_reseller_names = dict(
			(reseller.contact.username, reseller)
			for reseller in self._get_target_panel_api().list_resellers(reseller_logins)
		)

		logger.info(messages.GET_TARGET_PANELS_SERVICE_TEMPLATES_FOR)
		target_service_templates = []
		addon_service_templates = []
		for owner, plan_names in plans.iteritems():
			owner_id = ADMIN_ID
			if owner is not None:
				# TODO? assert owner in resellers? absense of plan's owner in resellers may mean corrupt logic of migrator
				if owner in target_reseller_names:
					owner_id = target_reseller_names[owner].id
				else:
					logger.debug(messages.NO_RESELLER_WITH_SUCH_LOGIN_ON_TARGET, owner)
					continue

			target_service_templates += self._get_target_panel_api().get_service_template_info_list([
				st.st_id for st in self._get_target_panel_api().get_service_template_list(owner_id, plan_names)
			])
			addon_service_templates += self._get_target_panel_api().get_addon_service_template_list(owner_id)

		logger.info(messages.GET_TARGET_PANELS_CUSTOMERS_FOR_CONFLICT)
		target_usernames = dict(
			(customer.contact.username, customer)
			for customer in self._get_target_panel_api().list_customers(client_logins)
		)

		logger.info(messages.GET_TARGET_PANELS_AUXILIARY_USERS_AND)
		plesk_auxiliary_users = {}
		plesk_auxiliary_user_roles = {}

		# here and below, chunk_size argument of partition_list() is set to 1, to work around PPP-9186
		for target_usernames_chunk in partition_list(target_usernames.keys(), chunk_size=1):
			plesk_customers = [result.data[1] for result in self.conn.target.plesk_api().send(
				plesk_ops.CustomerOperator.Get(
					filter=plesk_ops.CustomerOperator.FilterByLogin(target_usernames_chunk),
					dataset=[plesk_ops.CustomerOperator.Dataset.GEN_INFO],
				)
			)]
			plesk_auxiliary_users.update(dict(
				((customer.login, result.data.gen_info.login), result.data)
				for customer in plesk_customers
				for result in self.conn.target.plesk_api().send(
					plesk_ops.UserOperator.Get(
						filter=plesk_ops.UserOperator.FilterOwnerGuid([customer.guid]),
						dataset=[plesk_ops.UserOperator.Dataset.GEN_INFO, plesk_ops.UserOperator.Dataset.ROLES]
					)
				)
			))

			plesk_auxiliary_user_roles.update(dict(
				((customer.login, result.data.name), result.data)
				for customer in plesk_customers
				for result in self.conn.target.plesk_api().send(
					plesk_ops.RoleOperator.Get(
						filter=plesk_ops.RoleOperator.FilterAll(),
						owner_guid=customer.guid
					)
				)
			))

		admin_guid = get_admin_guid(self.conn.target.plesk_server)
		if admin_guid is not None:
			plesk_auxiliary_users.update({
				(None, result.data.gen_info.login): result.data
				for result in self.conn.target.plesk_api().send(
					plesk_ops.UserOperator.Get(
						filter=plesk_ops.UserOperator.FilterOwnerGuid([admin_guid]),
						dataset=[plesk_ops.UserOperator.Dataset.GEN_INFO, plesk_ops.UserOperator.Dataset.ROLES]
					)
				)
			})

			plesk_auxiliary_user_roles.update({
				(None, result.data.name): result.data
				for result in self.conn.target.plesk_api().send(
					plesk_ops.RoleOperator.Get(
						filter=plesk_ops.RoleOperator.FilterAll(),
						owner_guid=admin_guid
					)
				)
			})

		logger.info(messages.GET_TARGET_PANELS_SUBSCRIPTIONS_AND_WEBSPACES)
		target_webspaces = self._get_target_panel_api().list_webspaces(subscription_names)

		# Get target subscriptions belonging to the specified accounts:
		# 1. Accounts participating in migration
		# 2. Owners of webspaces participating in migration (these may be not among the accounts participating in migration)
		subscription_owner_ids = [cinfo.id for cinfo in target_usernames.itervalues()] + [webspace.owner_id for webspace in target_webspaces]
		raw_target_subscriptions = self._get_target_panel_api().list_hosting_subscriptions(subscription_owner_ids)

		# In addition to service templates retrieved by name, get service templates used by subscriptions retrieved above
		# (converter needs service template to check limits on resources in existing subscription, before adding a webspace into it).
		target_sts_by_id = group_by_id(target_service_templates, lambda st: st.st_id)
		target_service_templates += self._get_target_panel_api().get_service_template_info_list(
			service_template_ids=[
				sub.st_id for sub in raw_target_subscriptions
				if sub.st_id is not None and sub.st_id not in target_sts_by_id
			]
		)

		# Get sites, site_aliases and subdomains of the subscriptions that we're going to transfer
		# For the domain names that are not found in subscriptions + sites + site_aliases + subdomains,
		# find if each such domain exists on target panel. Checker will check 2 things:
		# 1. whether domain being transferred belongs to the same webspace on target panel
		# as subscription being transferred
		# 2. whether domain being transferred exists on target panel (just outside of subscriptions being
		# transferred, or outside of any target webspaces)
		logger.info(messages.GET_TARGET_PANELS_SUBSCRIPTIONS_SITES_AND)

		site_aliases = {}
		for domain_name in domain_names:
			site_aliases.update({
				result.data[1].name: result.data[1] for result in self.conn.target.plesk_api().send(
					plesk_ops.AliasOperator.Get(
						filter=plesk_ops.AliasOperator.FilterByName([domain_name])
					)
				) if not hasattr(result, 'code') or result.code != 1013  # require data, unless the object does not exist (1013)
			})

		subdomains = {}
		for domain_name in set(domain_names) - set(site_aliases.keys()):
			subdomains.update({
				result.data[1].name: result.data[1] for result in self.conn.target.plesk_api().send(
					plesk_ops.SubdomainOperator.Get(
						filter=plesk_ops.SubdomainOperator.FilterByName([domain_name])
					)
				) if not hasattr(result, 'code') or result.code != 1013
			})

		sites = []
		for site_name in (
			set(domain_names + [subdomain.parent for subdomain in subdomains.itervalues()])
			- set(subdomains.keys() + site_aliases.keys())
		):
			sites += [
				result for result in self.conn.target.plesk_api().send(
					plesk_ops.SiteOperator.Get(
						filter=plesk_ops.SiteOperator.FilterByName([site_name]),
						dataset=[plesk_ops.SiteOperator.Dataset.GEN_INFO]
					)
				) if not hasattr(result, 'code') or result.code != 1013
			]

		sites_by_id = {site.data[0]: site.data[1] for site in sites}
		for site_id in set([
			site_alias.site_id for site_alias in site_aliases.itervalues()
			if site_alias.site_id not in sites_by_id
		]):
			sites += [
				result for result in self.conn.target.plesk_api().send(
					plesk_ops.SiteOperator.Get(
						filter=plesk_ops.SiteOperator.FilterById([site_id]),
						dataset=[plesk_ops.SiteOperator.Dataset.GEN_INFO]
					)
				) if not hasattr(result, 'code') or result.code != 1013
			]

		# refresh mapping
		sites_by_id = {site.data[0]: site.data[1] for site in sites}
		sites_by_name = {site.data[1].gen_info.name: site.data[1] for site in sites}

		plesk_subscriptions = {}
		for subscription_name in (
			set(domain_names)
			- set(site_aliases.keys() + subdomains.keys() + sites_by_name.keys())
		):
			plesk_subscriptions.update({
				result.data[1].gen_info.name: result.data[1] for result in self.conn.target.plesk_api().send(
					plesk_ops.SubscriptionOperator.Get(
						plesk_ops.SubscriptionOperator.FilterByName([subscription_name]),
						[plesk_ops.SubscriptionOperator.Dataset.GEN_INFO]
					)
				) if not hasattr(result, 'code') or result.code != 1013
			})

		for subscription_guid in set([site.gen_info.webspace_guid for site in sites_by_name.itervalues()]):
			plesk_subscriptions.update({
				result.data[1].gen_info.name: result.data[1] for result in self.conn.target.plesk_api().send(
					plesk_ops.SubscriptionOperator.Get(
						plesk_ops.SubscriptionOperator.FilterByGuid([subscription_guid]),
						[plesk_ops.SubscriptionOperator.Dataset.GEN_INFO]
					)
				)
			})

		# refresh mapping
		subscriptions_by_guid = group_by_id(plesk_subscriptions.values(), lambda s: s.gen_info.guid)

		plesk_sites = {
			site_name: (subscriptions_by_guid[site.gen_info.webspace_guid].gen_info.name, site)
			for site_name, site in sites_by_name.iteritems()
		}

		plesk_site_aliases = {
			alias_name: (subscriptions_by_guid[sites_by_id[alias.site_id].gen_info.webspace_guid].gen_info.name, alias)
			for alias_name, alias in site_aliases.iteritems()
		}

		plesk_subdomains = {
			subdomain_name: (
				subscriptions_by_guid[sites_by_name[subdomain.parent].gen_info.webspace_guid].gen_info.name, subdomain
			)
			for subdomain_name, subdomain in subdomains.iteritems()
		}

		target_existing_objects = ExistingObjectsModel(
			resellers=target_reseller_names,
			customers=target_usernames,
			service_templates=target_service_templates,
			addon_service_templates=addon_service_templates,
			raw_target_subscriptions=raw_target_subscriptions,
			webspaces=target_webspaces,
			auxiliary_user_roles=plesk_auxiliary_user_roles,
			auxiliary_users=plesk_auxiliary_users,
			plesk_subscriptions=plesk_subscriptions,
			plesk_sites=plesk_sites,
			plesk_site_aliases=plesk_site_aliases,
			plesk_subdomains=plesk_subdomains,
			ip_addresses=self.conn.target.plesk_server.get_all_ips(self.global_context)
		)

		# XXX existing objects should not be stored in a file
		target_filename = self.session_files.get_path_to_existing_objects_model()
		write_yaml(target_filename, target_existing_objects)
		self.global_context.target_existing_objects = target_existing_objects
		return target_existing_objects

	def _read_migration_list_lazy(self, options):
		# XXX is a public method, used in parallels\common\workflow\shared_hosting_workflow.py(251)
		if not self.read_migration_list:
			try:
				self.global_context.migration_list_data = self._read_migration_list(options)
			except UnicodeDecodeError, e:
				logger.debug(messages.UNABLE_READ_MIGRATION_LIST_FILE, exc_info=e)
				raise MigrationError(
					messages.THERE_ARE_NONUTF_CHARACTERS_IN_MIGRATION)
			self.read_migration_list = True

			# now that the migration list is loaded, the dump - that was already
			# loaded and yields all available subscriptions - is not valid
			# anymore.  need to clear cache, so that the dump will be
			# reloaded, and filtered using the migration list.
			self.load_raw_dump.clear()

		if self.global_context.migration_list_data is None:
			# 2. from source dumps - reusing implementation of transfer list - when a transfer list is not defined.
			(migration_list_data, errors) = self._get_migration_list_class().generate_migration_list(
				self._get_migration_list_source_data(), webspace_names=[],
				service_templates={}
			)
			# if there were errors (e.g. because subscription is not mapped to a plan), it does not make sense to continue and fetch data from target.
			if len(errors) > 0:
				raise MigrationError(messages.MIGRATION_TOOL_NOT_ENOUGH_INFORMATION_TRANSFER)
		else:
			migration_list_data = self.global_context.migration_list_data

		return migration_list_data

	def _get_migration_list_source_data(self):
		return lambda: self.iter_shallow_dumps()

	def _read_migration_list(self, options, migration_list_optional=False):
		# migration_list_optional is necessary for PBAS migrator which allows not to specify migration list
		# in all the other cases migration list must be specified
		def reader_func(fileobj):
			panel = self._get_target_panel()
			return self._get_migration_list_class().read(
				fileobj,
				self._get_migration_list_source_data(),
				has_custom_subscriptions_feature=panel.has_custom_subscriptions_feature(),
				has_admin_subscriptions_feature=panel.has_admin_subscriptions_feature(),
				has_reseller_subscriptions_feature=panel.has_reseller_subscriptions_feature()
			)

		return self._read_migration_list_data(options, reader_func, migration_list_optional)

	def _read_migration_list_data(self, options, reader_func, migration_list_optional=False):
		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.debug(messages.LOG_EXCEPTION, exc_info=True)
			raise MigrationError(
				messages.FAILED_TO_READ_MIGRATION_LIST % (migration_list_file, str(e))
			)

		if len(errors) > 0:
			raise MigrationError(
				messages.THERE_WERE_SOME_ERRORS_WHILE_READING + "\n".join(errors)
			)

		return mapping

	def _get_safe_lazy(self, create_new=False):
		if self.safe is None:
			self.safe = Safe(self._get_target_model())
		elif self.safe.model is None:
			self.safe.model = self._get_target_model()
		return self.safe

	def _get_target_model(self):
		if os.path.exists(self.session_files.get_path_to_target_model()):
			return self._load_target_model(False)
		else:
			# if model does not exist - do not load it to make possible use safe in steps before 'convert'
			return None

	def convert(self, merge_pre_migration_report=True):
		self._read_migration_list_lazy(self.global_context.options)

		root_report = checking.Report(messages.REPORT_DETECTED_PROBLEMS, None)
		existing_objects = read_yaml(self.session_files.get_path_to_existing_objects_model())

		target = self._convert_model(root_report, existing_objects)

		if not self.global_context.options.ignore_pre_migration_errors:
			# stop migration if there is any tree issue at the 'error' level
			if root_report.has_errors():
				self.print_report(root_report, "convert_report_tree", show_no_issue_branches=False)
				raise MigrationError(
					messages.UNABLE_CONTINUE_MIGRATION_UNTIL_THERE_ARE +
					messages.PLEASE_REVIEW_PREMIGRATION_TREE_ABOVE_AND)

		if merge_pre_migration_report:
			# merge pre-migration check tree into final report tree
			safe = self._get_safe_lazy()

			for server_id, raw_dump in self.iter_panel_server_raw_dumps():
				for subscription, report in self._iter_subscriptions_by_report_tree(
					raw_dump, root_report.subtarget(u"Source server", server_id)
				):
					with subscription_context(subscription.name):
						for issue in report.issues:
							safe.add_issue_subscription(subscription.name, issue)

		return root_report, target

	def _convert_model(self, root_report, target_existing_objects):
		migration_list_data = self._read_migration_list_lazy(self.global_context.options)
		converted_resellers = self._convert_resellers(
			target_existing_objects.resellers,
			if_not_none(migration_list_data, lambda m: m.resellers),
			root_report
		)
		if self.global_context.options.command_name not in ['check']:
			converted_resellers_filtered = [
				reseller for reseller in converted_resellers if reseller.source == SOURCE_TARGET
			]
		else:
			converted_resellers_filtered = converted_resellers

		target_model = self._convert_accounts(converted_resellers_filtered, root_report)
		self._write_target_model(target_model)
		return target_model

	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.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.target_model = target_model
		if self.safe is not None:
			self.safe.model = target_model

	def _load_target_model(self, apply_filtering=True):
		"""Either get target model from memory, or load it from file.

		See _write_target_model function comment for more details.
		Also apply filtering if corresponding parameter is set to True for repeatability feature:
		objects (subscriptions, clients, resellers, plans) that have some problems should be
		skipped on all the futher steps.
		"""
		if self.target_model is None:
			try:
				self.target_model = read_yaml(self.session_files.get_path_to_target_model())
			except IOError as err:
				if err.errno == errno.ENOENT:
					raise MigrationError(messages.TARGET_MODEL_FILE_NOT_FOUND)
				else:
					raise

		if apply_filtering:
			safe = self._get_safe_lazy()
			return safe.get_filtering_model()
		else:
			return self.target_model

	def _load_reseller_mapping(self, model):
		reseller_logins = set([reseller.login for reseller in model.resellers.itervalues()])

		return dict(
			(reseller.contact.username, reseller.id)
			for reseller in self._get_target_panel_api().list_resellers(reseller_logins)
			if reseller.contact.username in reseller_logins
		)

	def _load_client_mapping(self, model):
		all_client_logins = set([
			client.login for client in model.iter_all_clients()
			if not client.is_admin()
		])

		return dict(
			(customer.contact.username, customer.id)
			for customer in self._get_target_panel_api().list_customers(all_client_logins)
			if customer.contact.username in all_client_logins
		)

	def set_security_policy(self):
		self._changed_security_policy_nodes = set()
		for server in self._get_nodes_to_set_security_policy():
			try:
				signature, security_policy_enabled = self._get_security_policy(server)
				if security_policy_enabled:
					self._changed_security_policy_nodes.add(server)
					self._set_security_policy(server, signature, False)
			except Exception as e:
				logger.debug(messages.LOG_EXCEPTION, exc_info=True)
				logger.error(messages.FAILED_SET_SECURITY_POLICY_SOME_TARGET % str(e))

	def restore_security_policy(self):
		try:
			for server in self._changed_security_policy_nodes:
				try:
					signature, security_policy_enabled = self._get_security_policy(server)
					if not security_policy_enabled:
						self._set_security_policy(server, signature, True)
				except Exception as e:
					logger.debug(messages.LOG_EXCEPTION, exc_info=True)
					logger.error(messages.FAILED_RESTORE_SECURITY_POLICY_TARGET_NODE % (server.description(), e,))
		except Exception as e:
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)
			logger.error(messages.FAILED_RESTORE_SECURITY_POLICY_SOME_TARGET % (e,))

	def _get_nodes_to_set_security_policy(self):
		return set(self._get_subscription_windows_web_nodes()) | set(self._get_subscription_windows_mssql_nodes())

	@staticmethod
	def _get_security_policy(server):
		"""
		:type server: parallels.core.connections.target_servers.TargetServer
		"""
		security_policy_sdl = server.get_session_file_path('security_policy.sdl')
		with server.runner() as runner:
			# secedit requires some file for export command, so we create and empty one
			runner.upload_file_content(security_policy_sdl, "")
			systemroot = runner.sh('cmd.exe /C "echo %systemroot%"').strip()
			runner.sh(
				'secedit /export /db "%s\Security\Database\secedit.sdb" /areas SECURITYPOLICY /cfg "%s"' % (
					systemroot, security_policy_sdl
				)
			)
			security_policy_content = runner.get_file_contents(security_policy_sdl)
			runner.remove_file(security_policy_sdl)
			# Example content of security_policy_content.sdl file:
			# [Version]
			# signature="$CHICAGO$"
			# Revision=1
			# [Profile Description]
			# Description=Default Security Settings. (Windows Server)
			# [System Access]
			# PasswordComplexity = 1

			regex = re.compile('^(\w+)\s*=\s*(.+)$', re.MULTILINE)
			matches = regex.findall(security_policy_content)
			signature = '"$CHICAGO$"'  # default value for signature
			password_policy_value = True  # default value for security policy
			for matched_item in matches:
				if matched_item[0] == 'signature':
					signature = matched_item[1].replace('\r', '')
				elif matched_item[0] == 'PasswordComplexity':
					password_policy_value = False if matched_item[1].replace('\r', '') == '0' else True

			return signature, password_policy_value

	@classmethod
	def _set_security_policy(cls, server, signature, required_policy):
		"""
		:type server: parallels.core.connections.target_servers.TargetServer
		:type signature: str
		:type required_policy: bool
		"""
		security_policy_sdl = server.get_session_file_path('security_policy.sdl')
		with server.runner() as runner:
			logger.debug(
				messages.DEBUG_ENABLE_SECURITY_POLICY if required_policy else messages.DEBUG_DISABLE_SECURITY_POLICY,
				server.description()
			)
			policy_template = cls._get_complexity_gpo_template(signature, required_policy)
			runner.upload_file_content(security_policy_sdl, policy_template)
			systemroot = runner.sh('cmd.exe /C "echo %systemroot%"').strip()
			runner.sh('secedit /validate "%s"' % security_policy_sdl)
			runner.sh(
				'secedit /configure /db "%s/Security/Database/secedit.sdb" /cfg "%s"' % (
					systemroot, security_policy_sdl
				)
			)
			runner.remove_file(security_policy_sdl)

	@staticmethod
	def _get_complexity_gpo_template(signature, required_policy):
		required_policy = '1' if required_policy else '0'
		lines = ['[Version]', 'signature=%s', '[System Access]', 'PasswordComplexity = %s']
		template = '\r\n'.join(lines)
		return template % (signature, required_policy)

	def convert_hosting(self, options):
		self._check_connections(options)
		self._read_migration_list_lazy(options)
		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(options)
		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_S, node.description())
			with node.runner() as runner:
				runner.sh(u'iisreset')

	def restore_status(self, options, finalize=True):
		self._read_migration_list_lazy(options)

		# we suspend customers and subscriptions after all subscriptions are provisioned,
		# otherwise we could have problems because CBM:
		# - does not provision subscriptions of suspended customer
		# - does not provision suspended subscriptions
		# We suspend customers and subscriptions after hosting settings restoration because provisioning is on during
		# status set operation and subdomains must be already in place.
		# Otherwise Apache/IIS configuration for subdomains
		# will be missing.

		model = self._load_target_model()

		target_client_ids = self._load_client_mapping(model)

		safe = self._get_safe_lazy()

		def suspend_subscriptions(subscriptions):
			for subscription in subscriptions:
				with safe.try_subscription(subscription.name, messages.FAILED_SUSPEND_SUBSCRIPTION):
					if subscription.source == SOURCE_TARGET:
						# Subscriptions that are already on target panel should not be touched
						continue

					if self.target_panel in [TargetPanels.PLESK, TargetPanels.PVPS]:
						# There is no need to suspend subscriptions when
						# migrating to Plesk they are already suspended by
						# Plesk restore
						continue

					if self.multiple_webspaces:
						# Webspace is suspended when Plesk restore suspends the
						# domain. In multiple webspaces mode we can't suspend
						# whole subscription as there could be other webspaces.
						# So, nothing to do there.
						continue

					if subscription.is_enabled:
						continue

					webspace_id = self._get_target_panel_api().get_webspace_id_by_primary_domain(subscription.name)
					logger.debug(messages.SUSPEND_TARGET_SUBSCRIPTION, subscription.name)
					subscription_id = self._get_target_panel_api().get_subscription_id_by_webspace_id(webspace_id)
					self._get_target_panel_api().suspend_subscription(subscription_id)

		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._get_target_panel_api().suspend_customer(target_client_ids[client.login])
					suspend_subscriptions(client.subscriptions)

		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.itervalues())
		suspend_resellers(model.resellers.itervalues())

	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 = self.global_context.import_api.get_domain_dns_server_ips(
			subscription.panel_target_server, domain_name
		)
		if len(dns_server_ips) == 0:
			raise Exception(messages.AT_LEAST_ONE_DNS_SERVER_SHOULD % domain_name)
		return dns_server_ips[0]

	def _import_resellers(self, options):
		# 1) read resellers from migration list
		resellers_migration_list = self._read_migration_list_resellers(options)
		# 2) for the resellers from migration list, fetch them from target panel if they exist there
		existing_resellers = self._fetch_resellers_from_target(
			if_not_none(resellers_migration_list, lambda r: r.keys()) or []
		)
		# 3) convert resellers to target model
		report = checking.Report(messages.REPORT_POTENTIAL_ISSUES, None)
		converted_resellers = self._convert_resellers(existing_resellers, resellers_migration_list, report)
		self._check_pre_migration_report(report, options)
		# 4) import resellers
		# TODO the whole Safe object is too complex for resellers only, write small implementation of Safe for resellers only
		safe = Safe(None)
		importer = ResellerImporter(self._get_target_panel_api(), safe)
		importer.import_resellers(converted_resellers)
		# 5) print final report and exit
		self._resellers_print_summary(converted_resellers, safe.failed_objects.resellers)
		self._resellers_exit_failed(safe.failed_objects.resellers)

	def _check_pre_migration_report(self, report, options):
		self.print_report(report, show_no_issue_branches=False) # always print report
		if not options.ignore_pre_migration_errors:
			# stop migration if there is any tree issue at the 'error' level
			if report.has_errors():
				raise MigrationError(
					messages.UNABLE_CONTINUE_MIGRATION_UNTIL_THERE_ARE_1 +
					messages.PLEASE_REVIEW_PREMIGRATION_TREE_ABOVE_AND_1)

	def _resellers_print_summary(self, converted_resellers, failed_resellers):
		"""Arguments:
		- 'converted_resellers' - list of target_model.Reseller objects
		- 'failed_resellers' - dict with key - login of failed reseller, value - list of safe.FailedObjectInfo
		"""
		report = checking.Report(messages.DETAILED_RESELLER_MIGRATION_STATUS, None)
		for reseller in converted_resellers:
			reseller_report = report.subtarget(u"Reseller", reseller.login)

			if reseller.login in failed_resellers:
				self._add_report_issues(
					reseller_report,
					failed_resellers[reseller.login]
				)

		self.print_report(report, 'resellers_report_tree', show_no_issue_branches=False)

		logger.info(messages.SUMMARY)
		logger.info(
			messages.OPERATION_FINISHED_SUCCESSFULLY_FOR_S_OUT,
			len(converted_resellers) - len(failed_resellers),
			len(converted_resellers)
		)

	def _resellers_exit_failed(self, failed_resellers):
		if len(failed_resellers) > 0:
			raise MigrationError(messages.FAILED_MIGRATE_SOME_RESELLERS_SEE_LOG)

	def _convert_resellers(self, 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_source_servers_info(), existing_resellers, resellers_migration_list,
			report, self.global_context.password_holder
		)

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

	def _fetch_resellers_from_target(self, reseller_logins):
		return dict(
			(reseller.contact.username, reseller)
			for reseller in self._get_target_panel_api().list_resellers(reseller_logins)
		)

	def _read_migration_list_resellers(self, options):
		return self._read_migration_list_data(
			options, lambda fileobj: self._get_migration_list_class().read_resellers(fileobj)
		)

	def _read_migration_list_plans(self, options):
		return self._read_migration_list_data(
			options, lambda fileobj: self._get_migration_list_class().read_plans(fileobj)
		)

	def transfer_resource_limits(self, options):
		raise MigrationError(messages.RESOURCE_LIMITS_TRANSFER_IS_AVAILABLE_ONLY)

	def transfer_billing(self, options):
		raise MigrationError(messages.BILLING_DATA_MIGRATION_IS_AVAILABLE_ONLY)

	def check_billing(self, options):
		raise MigrationError(messages.BILLING_DATA_MIGRATION_IS_AVAILABLE_ONLY_1)

	def compare_payments(self, options):
		raise MigrationError(messages.ESTIMATED_PAYMENTS_COMPARISON_IS_AVAILABLE_ONLY)

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

	def _print_dns_forwarding_undo_report(self, report):
		if report.has_issues():
			self.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)

	# ======================== post migration checks ==========================

	def _test_all(self, options):
		checks_source = CompoundHostingCheckSource([
			self._get_web_hosting_checks_data_source(),
			self._get_dns_hosting_checks_data_source(options),
			self._get_mail_hosting_checks_data_source(),
			self._get_users_hosting_checks_data_source(),
			self._get_database_hosting_checks_data_source()
		])
		self._run_hosting_checks(
			report_title=messages.TRANSFERRED_DOMAINS_FUNCTIONAL_ISSUES,
			checks_source=checks_source,
		)

	def _test_services(self, options):
		self.global_context.test_services_report = checking.Report(
			messages.REPORT_SERVICE_ISSUES, None
		)
		checks_source = ServiceCheckSource(
			self._get_target_panel().get_service_nodes(self.conn.target),
			self.conn.target
		)
		util_name = get_executable_file_name()
		check_hosting_checks_source(
			checks_source, self.global_context.test_services_report,
			hosting_check_messages.get_solutions(
				self._get_target_panel().get_hosting_check_messages_panel_id(),
				util_name
			),
			self._get_hosting_checkers_config()
		)

	def _test_sites(self, options):
		self._run_hosting_checks(
			report_title=messages.REPORT_WEBSITE_ISSUES,
			checks_source=self._get_web_hosting_checks_data_source(),
		)

	def _test_dns(self, options):
		self._run_hosting_checks(
			report_title=messages.TRANSFERRED_DOMAINS_DNS_ISSUES,
			checks_source=self._get_dns_hosting_checks_data_source(options),
		)

	def _test_mail(self, options):
		self._run_hosting_checks(
			report_title=messages.TRANSFERRED_DOMAINS_MAIL_ISSUES,
			checks_source=self._get_mail_hosting_checks_data_source(),
		)

	def _test_users(self, options):
		self._run_hosting_checks(
			report_title=u"Transferred Users Issues",
			checks_source=self._get_users_hosting_checks_data_source(),
		)

	def _test_databases(self, options):
		self._run_hosting_checks(
			report_title=messages.TRANSFERRED_DOMAINS_DATABASE_ISSUES,
			checks_source=self._get_database_hosting_checks_data_source(),
		)

	def _run_hosting_checks(
		self, report_title, checks_source,
	):
		self.global_context.post_migration_check_report = checking.Report(
			report_title, None
		)
		util_name = get_executable_file_name()
		check_hosting_checks_source(
			checks_source, self.global_context.post_migration_check_report,
			hosting_check_messages.get_solutions(
				self._get_target_panel().get_hosting_check_messages_panel_id(),
				util_name
			),
			self._get_hosting_checkers_config()
		)

	def _print_post_migration_check_report(self, report_filename, exit_with_exit_code=True):
		self._finalize_test(
			self.global_context.post_migration_check_report,
			report_filename,
			print_backup_hosting_report,
			exit_with_exit_code=exit_with_exit_code
		)

	def _print_test_services_report(self):
		self._finalize_test(
			self.global_context.test_services_report,
			'test_service_report',
			print_service_hosting_report
		)

	def _finalize_test(self, report, report_file_name, print_hosting_report, exit_with_exit_code=True):
		if report_file_name is not None:
			write_json(self._get_session_file_path('%s.json' % report_file_name), report.as_dictionary())
		print_hosting_report(
			report,
			save_report_function=lambda contents: migrator_utils.save_report(
				contents, self._get_session_file_path(report_file_name)
			),
			exit_with_exit_code=exit_with_exit_code
		)

	def _get_hosting_checkers_config(self):
		return MigratorHostingCheckersConfig(
			self.config, self.save_external_report_data
		)

	def save_external_report_data(self, filename, data):
		full_filename = self._get_session_file_path(filename)
		try:
			with open(full_filename, "w+") as f:
				f.write(data)
			return full_filename
		except IOError as e:
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)
			logger.error(messages.FAILED_TO_SAVE_EXTERNAL_REPORT % (full_filename, e))
			return None

	def _get_web_hosting_checks_data_source(self):
		"""Return object which provides web sites to check

		The returned object provides:
		- hierarchy (client-resellers-subscriptions-domains)
		- checks to perform
		"""
		def create_check_subscription_class(dump, subscription):
			return HostingObjectWebSubscription(
				dump, subscription, self._create_migrated_subscription
			)

		return ServerBackupsCheckSource(
			{server_id: raw_dump for server_id, raw_dump in self.iter_panel_server_raw_dumps()},
			create_check_subscription_class
		)

	def _get_mail_hosting_checks_data_source(self):
		def create_check_subscription_class(dump, subscription):
			return HostingObjectMailSubscription(
				None, subscription, self._create_migrated_subscription,
				True
			)

		return ServerBackupsCheckSource(
			{server_id: dump for server_id, dump in self.iter_converted_dumps()},
			create_check_subscription_class
		)

	def _get_dns_hosting_checks_data_source(self, options):
		def create_check_subscription_class(dump, subscription):
			return HostingObjectDNSSubscription(
				dump, subscription, self._create_migrated_subscription,
				skip_dns_forwarding_test=(
					not self._get_target_panel().is_test_dns_forwarding()
					or not self.global_context.source_has_dns_forwarding
					or options.skip_dns_forwarding_test
				)
			)

		return ServerBackupsCheckSource(
			{server_id: dump for server_id, dump in self.iter_converted_dumps()},
			create_check_subscription_class
		)

	def _get_users_hosting_checks_data_source(self):
		"""Return instance of UsersInfoSourceInterface"""
		def create_check_subscription_class(dump, subscription):
			return HostingObjectSubscriptionWithUsers(
				dump, subscription, self._create_migrated_subscription
			)

		return ServerBackupsCheckSource(
			{server_id: dump for server_id, dump in self.iter_converted_dumps()},
			create_check_subscription_class
		)

	def _get_database_hosting_checks_data_source(self):
		"""Return instance of DatabasesInfoSourceInterface"""
		class DatabasesInfoSource(DatabasesInfoSourceInterface):
			@staticmethod
			def list_databases_to_copy():
				return group_by(
					self._list_databases_to_copy(),
					lambda d: d.subscription_name
				)

			@staticmethod
			def get_source_mysql_client_cli(database_info):
				source = database_info.source_database_server
				if source.is_windows():
					return WindowsMySQLClientCLI(
						HostingCheckerRunnerAdapter(source.panel_server.runner),
						source.panel_server.get_path_to_mysql(),
						skip_secure_auth=source.panel_server.mysql_use_skip_secure_auth()
					)
				else:
					return UnixMySQLClientCLI(HostingCheckerRunnerAdapter(source.runner))

			@staticmethod
			@cached
			def list_target_panel_databases(subscription_name, database_type):
				plesk_api = self._get_target_panel().get_subscription_plesk_node(
					self.global_context,
					subscription_name
				).plesk_api()

				return plesk_api_utils.list_databases(plesk_api, subscription_name, database_type)

			@staticmethod
			def get_db_users(subscription_name, source_id, db_name, db_type):
				raw_dump = self.load_raw_dump(self.source_servers[source_id])
				subscription = raw_dump.get_subscription(subscription_name)
				users = []
				for user in itertools.chain(
					subscription.get_database_users(db_name, db_type),
					subscription.get_overall_database_users(db_type)
				):
					users.append(User(user.name, user.password, user.password_type))
				return users

		def create_check_subscription_class(dump, subscription):
			return HostingObjectDatabaseSubscription(
				dump, subscription, DatabasesInfoSource()
			)

		return ServerBackupsCheckSource(
			{server_id: raw_dump for server_id, raw_dump in self.iter_panel_server_raw_dumps()},
			create_check_subscription_class
		)

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

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

	def _get_mailserver_ip_by_subscription_name(self, source_settings, subscription_name):
		with closing(self.load_raw_dump(source_settings)) as raw_dump:
			raw_subscription_from_backup = raw_dump.get_subscription(subscription_name)
			source_mail_ipv4 = raw_subscription_from_backup.mail_service_ips.v4
			source_mail_ipv6 = raw_subscription_from_backup.mail_service_ips.v6
			if source_mail_ipv4 is None:
				# Mail service ips can absent in backup, for example for PfU8.
				# In this case we use server IP
				source_mail_ipv4 = source_settings.ip
			return Ips(source_mail_ipv4, source_mail_ipv6)

	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.
		Overriden in Helm 3 migrator which has fake domains.
		"""
		return False

	# ======================== end of post migration checks ===================

	def _subscription_is_windows(self, subscription_name, server_id=None):
		"""Return True if subscription is from a Plesk for Windows server
		(and thus should have an IIS web site) and False otherwise
		"""
		return self.source_servers[server_id].is_windows

	def _finalize(self, finalize, options):
		if finalize:
			self._print_summary(options)
			self._exit_failed_objects()

		unfiltered_model = self._load_target_model(False)
		safe = self._get_safe_lazy()

		len = 0
		for infos in safe.failed_objects.subscriptions.itervalues():
			if any([info.is_critical for info in infos]):
				len += 1
		if len == ilen(unfiltered_model.iter_all_subscriptions()) and len > 0:
			self._print_summary(options)
			raise MigrationError(messages.ALL_SUBSCRIPTIONS_ARE_FAILED_MIGRATE_STOPPING)

	def _exit_failed_objects(self):
		safe = self._get_safe_lazy()
		failed_objects_lists = [
			safe.failed_objects.subscriptions,
			safe.failed_objects.resellers,
			safe.failed_objects.clients,
			safe.failed_objects.plans,
			safe.failed_objects.auxiliary_users,
			safe.failed_objects.auxiliary_user_roles,
		]

		objects = [x.values() for x in failed_objects_lists]
		for obj_types in objects:
			for obj_type in obj_types:
				if self._get_report_obj_by_severity(obj_type, checking.Problem.ERROR):
					raise MigrationError(
						messages.FAILED_MIGRATE_SOME_OBJECTS_SEE_LOG)

	def _print_summary(self, options):
		unfiltered_model = self._load_target_model(False)
		safe = self._get_safe_lazy()

		# merge pre-migration check tree into final report tree
		plain_report = checking.PlainReport(
			self.global_context.pre_check_report, *self._extract_source_objects_info()
		)
		for subscription in self._iter_all_subscriptions():
			report = plain_report.get_subscription_report(
				subscription.model.source, subscription._name
			)
			for issue in report.issues:
				safe.add_issue_subscription(subscription._name, issue)

		steps_profiler.get_default_steps_report().set_migrated_subscriptions_count(
			ilen(unfiltered_model.iter_all_subscriptions())
		)

		self._print_summary_tree(unfiltered_model, safe)

		logger.info(messages.SUMMARY_1)

		if self.global_context.post_migration_checks_enabled:
			run_post_migrations_msg = "\n" + messages.RUN_TEST_ALL_AFTER_MIGRATION
		else:
			run_post_migrations_msg = ""

		logger.info(
			messages.SUBSCRIPTIONS_MIGRATION_STATUS + run_post_migrations_msg,
			ilen(unfiltered_model.iter_all_subscriptions()) - len(safe.failed_objects.subscriptions),
			ilen(unfiltered_model.iter_all_subscriptions())
		)

		def quote(x):
			return u"'%s'" % (x,)

		failed_objects_types = [
			(messages.FAILED_OBJECTS_SUBSCRIPTIONS, safe.failed_objects.subscriptions, quote),
			(messages.FAILED_OBJECTS_RESELLERS, safe.failed_objects.resellers, quote),
			(messages.FAILED_OBJECTS_CLIENTS, safe.failed_objects.clients, quote),
			(messages.FAILED_OBJECTS_PLANS, safe.failed_objects.plans, lambda p: (
				messages.OWNED_BY_RESELLER % (p[1], p[0]) if p[0] is not None else messages.OWNED_BY_ADMIN % p[1]
			)),
			(messages.FAILED_OBJECTS_AUX_USERS, safe.failed_objects.auxiliary_users, lambda a: (
				messages.OWNED_BY_CUSTOMER % (a[1], a[0],)
			)),
			(messages.FAILED_OBJECTS_AUX_USER_ROLES, safe.failed_objects.auxiliary_user_roles, lambda a: (
				messages.OWNED_BY_CUSTOMER % (a[1], a[0],)
			))
		]

		for name, objects, format_func in failed_objects_types:
			error_description = []
			for obj_type, obj_info_list in objects.iteritems():
				errors = self._get_report_obj_by_severity(
					obj_info_list, checking.Problem.ERROR
				)
				if len(errors) > 0:
					error_description.append(format_func(obj_type))
			if len(error_description) > 0:
				logger.error(
					messages.FAILED_TO_PERFORM_OPERATION,
					len(error_description), name, u', '.join(error_description))

		if (
			len(safe.failed_objects.subscriptions) > 0 and
			self.global_context.target_panel_obj.is_write_failed_subscriptions_list()
		):
			suffix = time.time()
			target_existing_objects = read_yaml(self.session_files.get_path_to_existing_objects_model())
			failed_subscriptions_filename = self._get_session_file_path("failed-subscriptions.%s" % (suffix,))
			successful_subscriptions_filename = self._get_session_file_path("successful-subscriptions.%s" % (suffix,))

			target_service_templates = {
				None: [
					st.name for st in target_existing_objects.service_templates
					if st.owner_id == ADMIN_ID
				]
			}
			for r in target_existing_objects.resellers.itervalues():
				target_service_templates[r.contact.username] = [
					st.name for st in target_existing_objects.service_templates if st.owner_id == r.id
				]

			error_messages = defaultdict(list)
			for subscription_name, error_infos in safe.failed_objects.subscriptions.iteritems():
				for error_info in error_infos:
					parts = []
					if error_info.error_message is not None:
						parts.append(error_info.error_message)
					if error_info.exception is not None:
						parts.append(messages.EXCEPTION_MESSAGE % (error_info.exception,))
					error_messages[subscription_name].extend(' '.join(parts).split("\n"))

			self._get_migration_list_class().write_selected_subscriptions(
				failed_subscriptions_filename,
				self._get_migration_list_source_data(),
				[webspace.name for webspace in target_existing_objects.webspaces],
				target_service_templates,
				if_not_none(self._read_migration_list_lazy(options), lambda ml: ml.subscriptions_mapping),
				safe.failed_objects.subscriptions,
				error_messages
			)
			self._get_migration_list_class().write_selected_subscriptions(
				successful_subscriptions_filename,
				self._get_migration_list_source_data(),
				[webspace.name for webspace in target_existing_objects.webspaces],
				target_service_templates,
				if_not_none(self._read_migration_list_lazy(options), lambda ml: ml.subscriptions_mapping),
				safe.failed_objects.subscriptions,
				{} # no error messages for successful subscriptions
			)

			logger.error(
				messages.SUBSCRIPTIONS_FAILED_TO_MIGRATE,
				len(safe.failed_objects.subscriptions),
				ilen(unfiltered_model.iter_all_subscriptions()),
				failed_subscriptions_filename,
				get_executable_file_name(),
				'transfer-accounts',
				failed_subscriptions_filename
			)

	@staticmethod
	def _get_report_obj_by_severity(obj_list, severity):
		return [info for info in obj_list if info.severity == severity]

	@classmethod
	def _add_report_issues(cls, report, object_info_list):
		for info in object_info_list:
			cls._add_report_item(report, info)

	@classmethod
	def _add_report_item(cls, report, info):
		problem = checking.Problem(None, info.severity, cls._format_object_info(info))
		solution = info.solution if info.solution is not None else ''
		report.add_issue(problem, solution)

	@classmethod
	def _format_object_info(cls, info):
		if info.exception is not None:
			if not isinstance(info.exception, MigrationError):
				return u"%s\nException: %s" % (info.error_message, info.exception)
			else:
				return u"%s\n%s" % (info.error_message, info.exception)
		else:
			return info.error_message

	def _print_summary_tree(self, unfiltered_model, safe):
		def add_auxiliary_roles_and_users_issues(client, client_report):
			for user in client.auxiliary_users:
				if (client.login, user.login) in safe.failed_objects.auxiliary_users:
					user_report = client_report.subtarget(u"Auxiliary user", user.login)
					for report_object in safe.failed_objects.auxiliary_users[(client.login, user.login)]:
						self._add_report_item(user_report, report_object)
			for role in client.auxiliary_user_roles:
				if (client.login, role.name) in safe.failed_objects.auxiliary_user_roles:
					role_report = client_report.subtarget(u"Auxiliary user role", role.name)
					self._add_report_issues(role_report, safe.failed_objects.auxiliary_user_roles[(client.login, role.name)])

		root_report = checking.Report(messages.REPORT_MIGRATION_STATUS, None)

		self._add_report_issues(root_report, safe.failed_objects.general)

		for (owner, plan_name), issues in safe.failed_objects.plans.iteritems():
			plan_report = root_report.subtarget(u"Plan", plan_name)
			self._add_report_issues(plan_report, issues)

		for reseller_name, reseller in unfiltered_model.resellers.iteritems():
			reseller_report = root_report.subtarget(u"Reseller", get_customer_name_in_report(reseller))

			if reseller_name in safe.failed_objects.resellers:
				self._add_report_issues(
					reseller_report,
					safe.failed_objects.resellers[reseller_name]
				)

			for client in reseller.clients:
				client_report = reseller_report.subtarget(u"Client", get_customer_name_in_report(client))

				if client.login in safe.failed_objects.clients:
					self._add_report_issues(
						client_report,
						safe.failed_objects.clients[client.login]
					)

				for subscription in client.subscriptions:
					subscription_report = client_report.subtarget(u"Subscription", subscription.name)
					if subscription.name in safe.failed_objects.subscriptions:
						self._add_report_issues(
							subscription_report,
							safe.failed_objects.subscriptions[subscription.name]
						)
					for issue in safe.issues.subscriptions[subscription.name]:
						subscription_report.add_issue_obj(issue)

				add_auxiliary_roles_and_users_issues(client, client_report)

		for client_login, client in unfiltered_model.clients.iteritems():
			if client.login is None:
				client_report = root_report.subtarget(u"Administrator", None)
			else:
				client_report = root_report.subtarget(u"Client", get_customer_name_in_report(client))
			if client_login in safe.failed_objects.clients:
				self._add_report_issues(
					client_report,
					safe.failed_objects.clients[client_login]
				)
			for subscription in client.subscriptions:
				subscription_report = client_report.subtarget(u"Subscription", subscription.name)
				if subscription.name in safe.failed_objects.subscriptions:
					self._add_report_issues(
						subscription_report,
						safe.failed_objects.subscriptions[subscription.name]
					)
				for issue in safe.issues.subscriptions[subscription.name]:
					subscription_report.add_issue_obj(issue)
			add_auxiliary_roles_and_users_issues(client, client_report)

		print ''  # put an empty line to output to segregate report from regular output
		self.print_report(root_report, 'accounts_report_tree')

	def _get_all_subscriptions_by_source(self, is_windows=None):
		target_model = self._load_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 print_report(self, report, report_file_name=None, show_no_issue_branches=True):
		if report_file_name is not None:
			write_json(self._get_session_file_path('%s.json' % report_file_name), report.as_dictionary())
		report_str = migrator_utils.format_report(
			report, self._get_minor_issue_targets(),
			if_not_none(report_file_name, self._get_session_file_path), show_no_issue_branches
		)
		print report_str
		if report_file_name is not None:
			path = migrator_utils.save_report(report_str, self._get_session_file_path(report_file_name))
			logger.info(messages.REPORT_WAS_SAVED_INTO_FILE_S % path)

	@classmethod
	def _get_minor_issue_targets(cls):
		"""Get names of issue target which must be hidden if empty.
		"""
		return [u'Auxiliary user', u'DNS zone']

	def transfer_subscription_databases(self, subscription):
		databases_by_subscription = group_by(
			self._list_databases_to_copy(),
			lambda db_info: db_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.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.database_name, db_info.subscription_name)

				def _repeat_copy_windows_content():
					rsync = self._get_rsync(
						db_info.source_database_server.utilities_server,
						db_info.target_database_server.utilities_server,
						db_info.source_database_server.utilities_server.ip()
					)
					mssql_copy_method = read_mssql_copy_method(self.global_context.config)
					database_utils.copy_db_content_windows(db_info, rsync, mssql_copy_method)

				safe.try_subscription_with_rerun(
					_repeat_copy_windows_content,
					db_info.subscription_name,
					messages.FAILED_COPY_CONTENT_DATABASE_S % db_info.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_CONTENT_DATABASE_S_MIGRATION_1 % (
						db_info.database_name),
					solution=messages.FIX_SERVICE_TEMPLATE_SUBSCRIPTION_IS_ASSIGNED_1,
					is_critical=False
				)
			else:
				repeat_error = messages.UNABLE_COPY_CONTENT_DATABASE_S_SUBSCRIPTION_1 % (
					db_info.database_name, db_info.subscription_name
				)

				def _repeat_copy_linux_content():
					keyfile = self.global_context.ssh_key_pool.get(
						db_info.source_database_server, db_info.target_database_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_CONTENT_DATABASE_S_1 % db_info.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.database_name.lower()
				target_databases = {db.lower() for db in target_databases}
			else:
				db_name_comparable = db_info.database_name

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

		return True

	def _list_target_server_databases(self, 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[basestring] | 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.debug(messages.LOG_EXCEPTION, exc_info=True)
			logger.error(
				messages.FAILED_TO_DETECT_DB_LIST, target_database_server.description(), str(e)
			)
			return None

	def _list_databases_to_copy(self):
		"""
		:rtype: list[parallels.core.utils.database_utils.DatabaseInfo]
		"""
		target_model = self._load_target_model()

		safe = self._get_safe_lazy()
		clients = target_model.clients.values() + sum((r.clients for r in target_model.resellers.values()), [])
		model_subscriptions = sum((c.subscriptions for c in clients), [])

		servers_subscriptions = group_by(model_subscriptions, lambda subscr: subscr.source)

		databases = list()
		for source_id, subscriptions in servers_subscriptions.iteritems():
			raw_dump = self.global_context.get_source_info(source_id).load_raw_dump()
			for subscription in subscriptions:
				with safe.try_subscription(subscription.name, messages.FAILED_DETECT_DATABASES_SUBSCRIPTION):
					backup_subscription = raw_dump.get_subscription(subscription.name)

					for db in backup_subscription.all_databases:
						if db.host == DATABASE_NO_SOURCE_HOST:
							continue

						source_database_server_model = self._get_src_db_server(db, raw_dump)

						target_database_server = self._get_target_database_server(subscription.name, db.dbtype)
						if target_database_server is None:
							raise Exception(
								messages.DB_NOT_COPIED_NO_DB_SERVER.format(db=db, subscr=subscription.name)
							)

						has_conflicts = CheckDatabaseConflicts.has_database_issues(
							self.global_context, self._create_migrated_subscription(subscription.name),
							backup_subscription, db, target_database_server,
							add_report_issues=False
						)
						if has_conflicts:
							logger.debug(messages.SKIP_COPY_DATABASE_DUE_TO_CONFLICT.format(
								db_name=db.name, db_type=db.dbtype
							))
							continue

						source_database_server = self._get_source_database_server(
							source_id, source_database_server_model
						)

						if db.dbtype == DatabaseServerType.MSSQL:
							same_database_servers = compare_mssql_servers(
								source_database_server, target_database_server
							)
						else:
							if source_database_server_model.host == 'localhost':
								source_host = self._get_source_db_node(source_id).ip()
							else:
								source_host = source_database_server_model.host
							same_host = target_database_server.ip() in resolve_all(source_host)
							same_port = source_database_server_model.port == target_database_server.port()
							same_database_servers = same_port and same_host

						# Assumptions:
						# 1) When detecting whether target and source servers are actually the same server
						# we consider that database server hostnames are resolved in the same way on source
						# main node and on the node where you run migration tool.
						# 2) We consider that customer won't register database server by different IP addresses in
						# source and target systems.
						#
						# Otherwise migration tool may not detect that servers are the same, and emit an error message.
						if same_database_servers:
							logger.debug(messages.DEBUG_DB_ASSIMILATION.format(db=db))
							continue

						databases.append(DatabaseInfo(
							source_database_server=source_database_server,
							target_database_server=target_database_server,
							subscription_name=subscription.name,
							database_name=db.name
						))

		return databases

	def _get_source_database_server(self, source_id, model):
		"""
		:type model: parallels.core.dump.data_model.DatabaseServer
		:type source_id: str
		"""
		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, model.dbtype, model.host, model.port, model.login, model.password
		)
		if external_db_server is not None:
			return external_db_server
		else:
			return source_db_node.get_database_server(model)

	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.conn.get_external_db_server_id(db_type, host)
		if external_server_id is not None:
			settings = self.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.conn.get_external_db_physical_server(external_server_id),
			)
		else:
			return None

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

	def _get_src_db_server(self, db, backup):
		# this method is overridden in H-Sphere's migrator, since it does not know source MSSQL server admin credentials
		source_db_servers = {(dbs.host, dbs.port): dbs for dbs in backup.iter_db_servers()}
		return source_db_servers[(db.host, db.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_S_IN_CONVERTED % domain_name
		)

	def _read_file(self, pathname):
		with open(pathname) as f:
			return f.read()

	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:
			for web_node in self._get_subscription_windows_web_nodes():
				try:
					self._get_target_panel_api().refresh_node_components(web_node)
				except Exception as e:
					# Just a warning, for most of our customers refresh components list operation is not actual.
					logger.warning(
						messages.FAILED_REFRESH_COMPONENTS_LIST_FOR_S,
						web_node.description(), e
					)
					logger.debug(messages.LOG_EXCEPTION, exc_info=True)
		except Exception as e:
			# Just a warning, for most of our customers refresh components list operation is not actual.
			logger.warning(
				messages.FAILED_REFRESH_COMPONENTS_LIST_FOR_WINDOWS, e
			)
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)

	def _get_subscription_windows_web_nodes(self):
		web_nodes = []
		for subscription in self._load_target_model().iter_all_subscriptions():
			subscription_web_node = self._get_subscription_nodes(subscription.name).web
			if subscription_web_node is not None and subscription_web_node.is_windows():
				if subscription_web_node not in web_nodes:
					web_nodes.append(subscription_web_node)
		return web_nodes

	def _get_subscription_windows_mssql_nodes(self):
		mssql_nodes = []
		for subscription in self.global_context.iter_all_subscriptions():
			target_database_server = self._get_target_database_server(subscription.name, 'mssql')
			if target_database_server is not None and target_database_server.physical_server is not None:
				mssql_nodes.append(target_database_server.physical_server)

		return mssql_nodes

	def _is_subscription_suspended_on_source(self, source_server, subscription_name):
		"""Detect if subscription is suspended on the source at that moment

		Arguments:
		- source_server - source web server where subscription is located
		- subscription_name - name of subscription

		By default consider all subscriptions are active. Implement in child
		classes.  Actually the function is used when copying mail content with
		IMAP - we can't copy mail messages for suspended subscriptions because
		we're not able to login by IMAP, so we should detect such
		subscriptions. At that moment means that we should use the most recent
		information from source panel, not cached Plesk backup.
		"""
		return False

	@classmethod
	def _get_migration_list_class(cls):
		return MigrationList

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

	def _use_psmailbackup(self):
		return self.target_panel == TargetPanels.PLESK

	# ======================== 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.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.conn.target.main_node_description())
			with self.conn.target.main_node_runner() as runner:
				target_model = self._load_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.error(
				messages.FAILED_CHECK_DISK_SPACE_REQUIREMENTS_FOR,
				self.conn.target.main_node_description(), e
			)
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)

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

	def check_infrastructure(self, options, show_skip_message=True):
		"""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(report, error_message):
				def report_internal_error(exception):
					report.add_issue(
						checking.Problem(
							'infrastructure_checks_internal_error', checking.Problem.WARNING,
							messages.INTERNAL_ERROR_S_MIGRATION_TOOL_SKIP % (error_message)
						), u""
					)
					logger.error(messages.ERROR_AND_EXCEPTION_MESSAGE, error_message, exception)

				try:
					yield
				except Exception as e:
					logger.debug(messages.LOG_EXCEPTION, exc_info=True)
					report_internal_error(e)

			report = checking.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_CONNECTIONS_DATABASE_SERVERS):
				target_db_servers = {d.target_database_server for d in self._list_databases_to_copy()}
				check_database_server_connections(report, target_db_servers)
			with safe(report, messages.FAILED_CHECK_MYSQL_REQUIREMENTS):
				self._check_mysql_max_allowed_packet(report)

			self.print_report(report, 'infrastructure_checks_report_tree')

			infrastructure_check_success = not report.has_errors()
		except Exception as e:
			logger.error(messages.FAILED_PERFORM_INFRASTRUCTURE_CHECKS_S_CHECK, e)
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)

		if not infrastructure_check_success:
			raise MigrationNoContextError(
				messages.THERE_ARE_INFRASTRUCTURE_ISSUES_THAT_SHOULD +
				(messages.IF_YOU_WANT_SKIP_INFRASTRUCTURE_CHECKS if show_skip_message else u"")
			)

	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_BETWEEN_SOURCE_AND):
			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_SOURCE_AND_1):
			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_BETWEEN_SOURCE_AND_2):
			self._check_unix_copy_db_content_scp_connections(checks, db_report)
			self._check_windows_copy_db_content_rsync_connections(checks, db_report)
			self._check_windows_copy_mssql_db_content(checks, db_report)

	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.iteritems():
			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.iteritems():
			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.iteritems():
			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.iteritems():
			source_nodes = []
			for source_node, content_info in subscriptions_by_source.iteritems():
				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)

			diskspace_checker = infrastructure_checks.UnixDiskSpaceChecker()
			diskspace_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.iteritems():
			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().iteritems():
			subscriptions_by_nodes[nodes.target].append((nodes.source, subscriptions))

		checker = infrastructure_checks.WindowsDiskSpaceChecker()

		for target_node, subscriptions_by_source in subscriptions_by_nodes.iteritems():
			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.iteritems():
			checker_source.check(source_node, mysql_databases=databases, report=report)

	def _check_windows_copy_mssql_db_content(self, checks, report):
		def check_function(nodes_pair_info, mssql_connection_data):
			checker = infrastructure_checks.MSSQLConnectionChecker()

			script_filename = 'check_mssql_connection.ps1'
			local_script_filepath = get_package_extras_file_path(parallels.core, script_filename)

			return checker.check(
				nodes_pair_info,
				mssql_connection_data,
				local_script_filepath,
				script_filename
			)

		subscriptions = self._get_infrastructure_check_lister().get_subscriptions_by_mssql_instances().items()

		checks.check_mssql_connection(
			subscriptions=subscriptions,
			report=report,
			check_function=check_function
		)

	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_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.UnixMysqlMaxAllowedPacketChecker()
			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.WindowsMysqlMaxAllowedPacketChecker()
			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)"""
				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._get_source_mail_node(subscription_name)

			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._load_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.session_files.get_raw_dump_filename(server_id)

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

	@cached
	def load_shallow_dump(self, server_settings):
		if self.shallow_dump_supported(server_settings.id):
			session_files = self.global_context.session_files
			backup_filename = session_files.get_path_to_shallow_plesk_backup(
				server_settings.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(server_settings)
		else:
			return self.load_raw_dump(server_settings)

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

	@cached
	def load_raw_dump(self, server_settings):
		backup_filename = self.get_raw_dump_filename(server_settings.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(server_settings.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.source_servers.iterkeys():
			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.source_servers.iterkeys():
			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_mail_node(self, subscription_name):
		subscription = find_only(
			self._load_target_model().iter_all_subscriptions(),
			lambda s: s.name == subscription_name,
			error_message=messages.FAILED_FIND_SUBSCRIPTION_BY_NAME)
		source_node_id = subscription.source
		return self._get_source_node(
			self._get_mail_server_id(source_node_id)
		)

	def _get_source_web_node(self, subscription_name):
		subscription = find_only(
			self._load_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_RETRIVE_SOURCE_NODE_ID_S % node_id)
		return SourceServer(node_id, source_servers[node_id], self._get_migrator_server())

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

	def _get_mail_server_id(self, server_id):
		"""Overridden in Expand migrator"""
		return server_id

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

	def iter_shallow_dumps(self):
		for server_id, server_settings in self.source_servers.iteritems():
			with closing(self.load_shallow_dump(server_settings)) as backup:
				yield server_id, backup

	def iter_panel_server_raw_dumps(self):
		"""Iterate on the servers specified in the config option 'source-servers'"""
		for server_id, server_settings in self.source_servers.iteritems():
			with closing(self.load_raw_dump(server_settings)) as raw_dump:
				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 'source-servers'."""
		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_RETRIVE_ROW_BACKUP_S % 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().iteritems():
			with closing(self.load_raw_dump(server_settings)) as raw_dump:
				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_RETRIVE_ROW_BACKUP_S_1 % backup_id)

	def iter_converted_dumps(self):
		for server_id, server_settings in self.source_servers.iteritems():
			with closing(self.load_converted_dump(server_settings.id)) as converted_dump:
				yield server_id, converted_dump

	def get_converted_dump(self, backup_id):
		for current_backup_id, backup in self.iter_converted_dumps():
			if current_backup_id == backup_id:
				return backup
		raise Exception(messages.UNABLE_RETRIVE_CONVERTED_BACKUP_S % backup_id)

	def iter_converted_backups(self):
		for server_id, server_settings in self._get_source_servers().iteritems():
			with closing(self.load_converted_dump(server_settings.id)) as backup:
				yield server_id, backup

	def get_converted_backup(self, backup_id):
		for current_backup_id, backup in self.iter_converted_backups():
			if current_backup_id == backup_id:
				return backup
		raise Exception(messages.UNABLE_RETRIVE_CONVERTED_BACKUP_S_1 % backup_id)

	@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

	def _extract_source_objects_info(self):
		"""Return the tuple: (
			customers - { server_id: { login: [ (parent_type, parent_name), ] } },
			subscriptions - { server_id: { name: [ (parent_type, parent_name), ] } },
			servers - { server_id: product_name }
		)

		Implementation is product-specific.
		"""
		customers = defaultdict(lambda: defaultdict(list))  # { server_id: { login: [ parent, ] } }
		subscriptions = defaultdict(lambda: defaultdict(list))  # { server_id: { name: [ parent, ] } }
		servers = {id: 'Source' for id in self.source_servers.keys()}
		for server_id, raw_dump in self.iter_all_server_raw_dumps():
			for subscription in raw_dump.iter_admin_subscriptions():
				subscriptions[server_id][subscription.name] = []
			for client in raw_dump.iter_clients():
				customers[server_id][client.login] = []
				for subscription in client.subscriptions:
					subscriptions[server_id][subscription.name] = [obj(type='Client', name=client.login)]

			for reseller in raw_dump.iter_resellers():
				for subscription in reseller.subscriptions:
					subscriptions[server_id][subscription.name] = [obj(type='Reseller', name=reseller.login)]
				for client in reseller.clients:
					customers[server_id][client.login] = [obj(type='Reseller', name=reseller.login)]

					for subscription in client.subscriptions:
						subscriptions[server_id][subscription.name] = [
							obj(type='Reseller', name=reseller.login),
							obj(type='Client', name=client.login)
						]
		return customers, subscriptions, servers
