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

from parallels.common.registry import Registry
from parallels.common.utils.plesk_utils import get_database_subscription
from parallels.utils import obj
from parallels.utils.ip import is_ipv4
from parallels.utils.ip import resolve_all
from parallels.utils import ilen, group_by, if_not_none, cached, strip_multiline_spaces, group_by_id, is_run_on_windows
from parallels.common.connections.connections import Connections
from parallels.common.plesk_backup import plesk_backup_xml
from parallels.common.migrator_config import MailContent
from parallels.common.migrator_config import is_transfer_resource_limits_enabled
from parallels.common import MigrationNoContextError
from parallels.common.logging_context import log_context
from parallels.common.utils import plesk_api_utils
from parallels.common.utils import poa_utils
from parallels.plesk_api import operator as plesk_ops
from parallels.plesk_api.operator.subscription import Ips, SubscriptionHostingNone
from parallels import poa_api
from parallels.common.migration_list import MigrationList
from .importer import Importer
from parallels.common.utils.yaml_utils import write_yaml, read_yaml, pretty_yaml
from . import checking
import parallels.common.utils.restore_hosting_utils
from .existing_objects_model import ExistingObjectsModel
from parallels.common.utils.migrator_utils import trace, get_customer_name_in_report
from parallels.utils import partition_list, get_executable_file_name
from parallels.utils import open_unicode_file
from parallels.common.utils import steps_profiler
from parallels.common import run_command
from parallels.common.utils.config_utils import ConfigSection, config_get
from parallels.common.utils import migrator_utils, database_utils
from parallels.common.hosting_check.reporting import \
	print_backup_hosting_report, print_service_hosting_report
from parallels.utils import find_only
from parallels.hosting_check import User
from parallels.hosting_check.checkers.mysql_database_checker import \
	UnixMySQLClientCLI, WindowsMySQLClientCLI
from parallels.common.target_panels import TargetPanels
from parallels.common.migrated_subscription import MigratedSubscription
from parallels.hosting_check import messages
from parallels.common.hosting_check import check_hosting_checks_source
from parallels.common.hosting_check.entity_source.common import \
	ServerBackupsCheckSource
from parallels.common.hosting_check.entity_source.service import \
	ServiceCheckSource
from parallels.common.hosting_check.entity_source.web import \
	HostingObjectWebSubscription
from parallels.common.hosting_check.entity_source.mail import \
	HostingObjectMailSubscription
from parallels.common.hosting_check.entity_source.dns import \
	HostingObjectDNSSubscription
from parallels.common.hosting_check.entity_source.database import \
	HostingObjectDatabaseSubscription, DatabasesInfoSourceInterface
from parallels.common.hosting_check.entity_source.users import \
	HostingObjectSubscriptionWithUsers
from parallels.common.hosting_check.entity_source.compound import \
	CompoundHostingCheckSource
from parallels.common.hosting_check.utils.runner_adapter import \
	HostingCheckerRunnerAdapter
from parallels.common.hosting_check.config import \
	MigratorHostingCheckersConfig
from parallels.common.connections.migrator_server import MigratorServer
from parallels.common.session_files import CommonSessionFiles
from parallels.common.actions.backup.unpack import Unpack as ActionUnpackBackups
from parallels.common.connections.source_server import SourceServer
from parallels.common.global_context import GlobalMigrationContext
from parallels.plesks_migrator.infrastructure_checks.checks import NodesPair, check_database_server_connections
from parallels.plesks_migrator.infrastructure_checks import checks as infrastructure_checks
from parallels.plesks_migrator.infrastructure_checks.lister import \
	InfrastructureCheckListerDataSource, InfrastructureCheckLister
from parallels.common.converter.business_objects.password_holder import PasswordHolder
from parallels.common.converter.business_objects.resellers import ResellersConverter
from parallels.common.workflow.runner import create_action_runner
from parallels.common.utils.ssh_key_pool import SSHKeyPool
from parallels.common.utils.rsync_pool import RsyncPool
from . import MigrationError, run_and_check_local_command
from .safe import Safe


class LicenseValidationError(MigrationError):
	pass

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


class DatabaseInfo(object):
	"""Information about database to copy"""

	def __init__(self, source_database_server, target_database_server, subscription_name, database_name):
		self._source_database_server = source_database_server
		self._target_database_server = target_database_server
		self._subscription_name = subscription_name
		self._database_name = database_name

	@property
	def source_database_server(self):
		"""Source database server of database to copy

		:rtype: parallels.common.connections.database_server.DatabaseServer
		"""
		return self._source_database_server

	@property
	def target_database_server(self):
		"""Target database server of database to copy

		:rtype: parallels.common.connections.database_server.DatabaseServer
		"""
		return self._target_database_server

	@property
	def subscription_name(self):
		"""Name of subscription database belongs to

		:rtype: basestring
		"""
		return self._subscription_name

	@property
	def database_name(self):
		"""Name of database to copy

		:rtype: basestring
		"""
		return self._database_name

	def __str__(self):
		return "Database '%s' of subscription '%s': copy from %s to %s" % (
			self.database_name, self.subscription_name,
			self.source_database_server.description(),
			self.target_database_server.description()
		)

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')

SubscriptionTargetServices = namedtuple('SubscriptionTargetServices', (
	'web_ips',  # instance of Ips
	'mail_ips',  # instance of Ips
	'db_servers',  # dictionary with key - database server type, value - database server info
	'dns_ips'  # list of Ips
))

logger = logging.getLogger(__name__)


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

		logger.debug(u"START: %s %s" % (sys.argv[0], " ".join("'%s'" % arg for arg in sys.argv[1:])))
		logger.info(u"Initialize migrator")
		self.config = config

		self._load_configuration()
		
		steps_profiler.configure_report_locations(
			steps_profiler.get_default_steps_report(), 
			self._get_migrator_server()
		)

		self.read_migration_list = False
		self.ppa_model = None
		self.safe = None
		if self.target_panel == TargetPanels.PPA:
			self.webmail_ipv4 = self._get_webmail_ipv4()
		else:
			self.webmail_ipv4 = None

		self._changed_security_policy_nodes = None

		self.external_db_servers = {}
		self.source_plesks = self.conn.get_information_servers()
		self.session_files = self._create_session_files()

		self.global_context = Registry.get_instance().set_context(self._create_global_context())
		self.global_context.conn = self.conn
		self.global_context.session_files = self.session_files
		self.global_context.migrator_server = self._get_migrator_server()
		self.global_context.import_api = self._get_target_panel_api()
		self.global_context.safe = self._get_safe_lazy()
		self.global_context.webmail_ipv4 = self.webmail_ipv4
		self.global_context.target_panel = self.target_panel
		self.global_context.target_panel_obj = self._get_target_panel()
		self.global_context.load_raw_plesk_backup = self.load_raw_plesk_backup
		self.global_context.load_converted_plesk_backup = self.load_converted_plesk_backup
		self.global_context.load_shallow_plesk_backup = self.load_shallow_plesk_backup
		self.global_context.source_plesks = self.source_plesks
		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.migrator = self
		self.global_context.config = config
		self.global_context.migration_list_data = None

		self.global_context.apache_restart_interval = self._get_apache_restart_interval()

		self.global_context.pre_check_report = checking.Report(
			u"Detected potential issues", None
		)
		self.global_context.dns_forwarding_report = checking.Report(
			u"DNS forwarding issues", None
		)

		self.action_runner = create_action_runner(self.global_context)

		self.workflow = self._create_workflow()
		# extend workflow with target-panel specific actions
		self.global_context.target_panel_obj.extend_workflow(self.workflow)

	def run(self, entry_point):
		self.action_runner.run_entry_point(
			self.workflow.get_entry_point(entry_point)
		)

	def _create_workflow(self):
		raise NotImplementedError()

	def _get_rsync(self, source_server, target_server, source_ip=None):
		raise NotImplementedError()

	def set_options(self, options):
		self.global_context.options = options

	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):
		# XXX actually these are not just source Plesks, but other source
		# servers types too
		return self.source_plesks

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

	def _get_webmail_ipv4(self):
		webmail_ipv4 = self.config.get('ppa', 'webmail-ip')
		if not is_ipv4(webmail_ipv4):
			raise MigrationError(
				"Webmail IP address '%s' specified in 'webmail-ip' option of '[ppa]' is not valid IP v4. "
				"Please specify valid IP v4 address." % webmail_ipv4
			)
		return webmail_ipv4

	@cached
	def _get_target_panel(self):
		if self.target_panel == TargetPanels.PPA:
			from parallels.target_panel_ppa.panel import PPATargetPanel
			return PPATargetPanel()
		elif self.target_panel == TargetPanels.PLESK:
			from parallels.target_panel_plesk.panel import PleskTargetPanel
			return PleskTargetPanel()
		elif self.target_panel == TargetPanels.PVPS:
			from parallels.target_panel_pvps.panel import PVPSTargetPanel
			return PVPSTargetPanel()

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

	def _get_importer(self, safe):
		return Importer(self._get_target_panel_api(), self.conn.target, safe)

	def _load_configuration(self):
		logger.info(u"Load configuration")

		self.target_panel = self._load_configuration_target_panel()
		logger.debug("Target panel: %s", self.target_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(u"Invalid value '%s' is specified for 'transfer-domains-to-subscription' setting in the GLOBAL section of tool's configuration file, should be one of: %s (case-insensitive)." % (transfer_domains_mode, ",".join(transfer_domains_modes)))
		self.multiple_webspaces = transfer_domains_mode == 'same'

	def _get_apache_restart_interval(self):
		if self.target_panel in [TargetPanels.PPA, TargetPanels.PPA_WEB_SN, TargetPanels.PVPS]:
			section = 'ppa'
		else:
			section = 'plesk'

		if self.target_panel in [TargetPanels.PPA, TargetPanels.PPA_WEB_SN]:
			default = 'none'
		else:
			default = '1800'

		target_section = ConfigSection(self.config, section)
		return target_section.get('apache-restart-interval', default)

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

	def _load_configuration_target_panel(self):
		target_type = config_get(self.config, 'GLOBAL', 'target-type', default='ppa')
		if target_type == 'plesk':
			return TargetPanels.PLESK 
		elif target_type == 'ppa':
			return TargetPanels.PPA
		elif target_type == 'ppa-web-sn':
			return TargetPanels.PPA_WEB_SN
		elif target_type == 'pvps':
			return TargetPanels.PVPS
		else:
			raise MigrationError(strip_multiline_spaces("""
				Invalid target panel is specified in config.ini in 'target-type' option of '[GLOBAL]' section. 
				Specify one of: 
				- 'plesk', if you are going to migrate to Parallels Plesk
				- 'ppa', if you are going to migrate to Parallels Plesk Automation
				- 'ppa-web-sn', if you are going to move webspace between Parallels Plesk Automation
				- 'pvps', if you are going to migrate to dedicated Parallels Plesk
				  Shared Hosting Web Nodes
			"""))

	def _load_connections_configuration(self):
		return Connections(self.config, 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):
		subscription_target_services = self._get_subscription_target_services_cached().get(
			subscription_name
		)
		return self._get_target_panel().get_subscription_nodes(
			self.global_context, 
			subscription_target_services,
			subscription_name
		)

	@cached
	def _get_subscription_target_services_cached(self):
		if not self.conn.target.is_windows:
			with self.conn.target.main_node_runner() as runner:
				# this is necessary to get destination database server password
				with migrator_utils.plain_passwords_enabled(runner):
					return self._get_subscription_target_services()
		else:
			return self._get_subscription_target_services()

	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

		# 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(u"Skip checking panel migrator updates")
			return

		try:
			logger.info(u"Check panel migrator updates")
			available_version = urlopen('http://autoinstall.plesk.com/panel-migrator/version').read().strip()
			with open('/etc/panel-migrator/version', 'r') as version_file:
				installed_version = version_file.read().strip()
		except Exception as e:
			logger.debug(u'Exception:', exc_info=e)
			logger.info(
				u"Failed to check migration tools updates\n"
				u"Please perform check updates manually:\n"
				u"wget http://autoinstall.plesk.com/panel-migrator/installer.sh\n"
				u"chmod +x ./installer.sh\n"
				u"./installer.sh --check-updates"
			)
			return

		if not available_version > installed_version:
			logger.info(u"No updates available")
			return

		logger.info(u"A new version of migration tools already exists: %s is installed, %s is available. Updating to the new version..." % (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(u"The migration tools were updated to the latest version, please re-run it with the last command and arguments")
			sys.exit(0)
		except Exception as e:
			logger.debug(u'Exception:', exc_info=e)
			logger.info(
				u"Failed to upgrade migration tools\n"
				u"Please perform upgrade manually:\n"
				u"wget http://autoinstall.plesk.com/panel-migrator/installer.sh\n"
				u"chmod +x ./installer.sh\n"
				u"./installer.sh --upgrade"
			)

	def _check_target(self):
		try:
			logger.debug(u"Check connections to target nodes")
			self.global_context.conn.target.check_connections()
			logger.debug(u"Check Plesk version of the 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(u"Check log priority on target Plesk node")
				self._check_plesk_log_priority()
		except MigrationNoContextError:
			raise
		except Exception as e:
			logger.debug(u'Exception:', exc_info=e)
			raise MigrationError(u"Error while checking target panel configuration and connections: '%s'" % e)

	@trace('check-target-services', u'check services on target servers')
	def _check_target_services(self):
		try:
			logger.debug(u"Check connections on target nodes")
			self.global_context.test_services_report = checking.Report(
				u"Services' 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,
				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(u'Exception:', exc_info=e)
			logger.error(
				u"Failed to check services: %s. Check debug.log for more details. "
				u"Still migration will proceed to the next steps.", 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(u"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
			filename = plesk_root + '/admin/conf/panel.ini'
			exit_code, panel_ini, _  = runner.run_unchecked('cat', [filename])
			if exit_code != 0:
				logger.debug(u"Unable to cat %s file. Most probably this file is absent." % filename)
				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(
						u"Please change value of '%s' option in %s section in '%s' file on PPA management node to '0'. " 
						"If you want to debug PPA and run migration tool "
						"specify 'skip-log-priority-check' option in [GLOBAL] section of config file of migration tool, "
						" but migration tool can hang in this case."
						% (option_name, h_section_name, filename)
					)

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

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

	def _fetch(self, options, overwrite_source_data):
		# Determine the objects information for which we need to retrieve from PPA
		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 backups, to extract from them all domain_names to be transferred.
		domain_names = []
		for plesk_id, _ in self._iter_source_backups_paths():
			with closing(self.load_raw_plesk_backup(self.source_plesks[plesk_id])) as backup:
				for subscription in backup.iter_all_subscriptions():
					domain_names.append(subscription.name)
					domain_names += [ site.name for site in backup.iter_sites(subscription.name) ]
					domain_names += [ alias.name for alias in backup.iter_aliases(subscription.name) ]

		self._fetch_ppa_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_ppa_existing_objects(self, reseller_logins, client_logins, plans, subscription_names, domain_names):
		"""Fetch information about objects existing in PPA,
		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 PPA webspaces with these names
		@param domain_names { domain_name } - fetch Plesk subscriptions, sites, subdomains and site
			aliases with these domain names
		"""
		logger.info(u"Get target panel's resellers for conflict resolution")
		ppa_reseller_names = dict(
			(reseller.contact.username, reseller)
			for reseller in self._get_target_panel_api().list_resellers(reseller_logins)
		)

		logger.info(u"Get target panel's service templates for conflict resolution")
		ppa_service_templates = []
		addon_service_templates = []
		for owner, plan_names in plans.iteritems():
			owner_id = poa_api.Identifiers.OID_ADMIN
			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 ppa_reseller_names:
					owner_id = ppa_reseller_names[owner].id
				else:
					logger.debug(u"There is no reseller named %s in PPA, skipped retrieving its plans", owner)
					continue

			ppa_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(u"Get target panel's customers for conflict resolution")
		ppa_usernames = dict(
			(customer.contact.username, customer)
			for customer in self._get_target_panel_api().list_customers(client_logins)
		)

		logger.info(u"Get target panel's auxiliary users and roles for conflict resolution")
		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 ppa_usernames_chunk in partition_list(ppa_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(ppa_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
					)
				)
			))

		logger.info(u"Get target panel's subscriptions and webspaces for conflict resolution")
		ppa_webspaces = self._get_target_panel_api().list_webspaces(subscription_names)
	
		# Get PPA 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 ppa_usernames.itervalues() ] + [ webspace.owner_id for webspace in ppa_webspaces ]
		raw_ppa_subscriptions = self._get_target_panel_api().list_hosting_subscriptions(subscription_owner_ids)

		subscription_ids = set([subscription.subscription_id for subscription in raw_ppa_subscriptions])
		hanged_webspaces = []
		for webspace in ppa_webspaces:
			if webspace.subscription_id not in subscription_ids:
				hanged_webspaces.append((webspace.webspace_id, webspace.name))

		if len(hanged_webspaces) > 0:
			for (webspace_id, webspace_name) in hanged_webspaces:
				logger.error(strip_multiline_spaces("""
					There is inconsistency in PPA: webspace '{webspace_name}' ({webspace_id}) doesn't have corresponding subscription.
					The inconsistency must be fixed before migration.
					If you don't need that webspace in PPA and want to replace it with webspace from source panel
					run '{util_name} remove-webspace config.ini --webspace-id {webspace_id}' command to clean up hung webspace from PPA.
					""").format(
						util_name=get_executable_file_name(),
						webspace_id=webspace_id,
						webspace_name=webspace_name
					)
				)
			raise MigrationError("Fix the inconsistencies listed above and re-run migration tool when all of them are fixed")

		# 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).
		ppa_sts_by_id = group_by_id(ppa_service_templates, lambda st: st.st_id)
		ppa_service_templates += self._get_target_panel_api().get_service_template_info_list(
			service_template_ids=[
				sub.st_id for sub in raw_ppa_subscriptions 
				if sub.st_id is not None and sub.st_id not in ppa_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 in PPA. Checker will check 2 things:
		# 1. whether domain being transferred belongs to the same webspace in PPA as subscription being transferred
		# 2. whether domain being transferred exists in PPA (just outside of subscriptions being transferred, or outside of any PPA webspaces)
		logger.info(u"Get target panel's subscriptions, sites and site aliases for conflict resolution")
	
		site_aliases = {}
		for domain_names_chunk in partition_list(list(set(domain_names)), chunk_size=1):
			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_names_chunk)
					)
				) if not hasattr(result, 'code') or result.code != 1013	# require data, unless the object does not exist (1013)
			})

		subdomains = {}
		for domain_names_chunk in partition_list(list(set(domain_names) - set(site_aliases.keys())), chunk_size=1):
			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_names_chunk)
					)
				) if not hasattr(result, 'code') or result.code != 1013
			})

		sites_by_name = {}
		sites = []
		for site_names_chunk in partition_list(
			list(set(domain_names + [ subdomain.parent for subdomain in subdomains.itervalues() ])
			   - set(subdomains.keys() + site_aliases.keys())
			),
			chunk_size=1
		):
			sites += [
				result for result in self.conn.target.plesk_api().send(
					plesk_ops.SiteOperator.Get(
						filter=plesk_ops.SiteOperator.FilterByName(site_names_chunk),
						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_ids_chunk in partition_list(list(set([
			site_alias.site_id for site_alias in site_aliases.itervalues() if site_alias.site_id not in sites_by_id
		])), chunk_size=1):
			sites += [
				result for result in self.conn.target.plesk_api().send(
					plesk_ops.SiteOperator.Get(
						filter=plesk_ops.SiteOperator.FilterById(site_ids_chunk),
						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 }	# refresh mapping
		sites_by_name = { site.data[1].gen_info.name: site.data[1] for site in sites }

		subscriptions_by_name = {}
		for subscription_names_chunk in partition_list(list(set(domain_names) - set(site_aliases.keys() + subdomains.keys() + sites_by_name.keys())), chunk_size=1):
			subscriptions_by_name.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_names_chunk),
						[ plesk_ops.SubscriptionOperator.Dataset.GEN_INFO ]
					)
				) if not hasattr(result, 'code') or result.code != 1013
			})
		subscriptions_by_guid = group_by_id(subscriptions_by_name.values(), lambda s: s.gen_info.guid)

		for subscription_guids_chunk in partition_list(list(set([ site.gen_info.webspace_guid for site in sites_by_name.itervalues() ])), chunk_size=1):
			subscriptions_by_name.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_guids_chunk),
						[ plesk_ops.SubscriptionOperator.Dataset.GEN_INFO ]
					)
				)
			})
		subscriptions_by_guid = group_by_id(subscriptions_by_name.values(), lambda s: s.gen_info.guid)	# refresh mapping

		plesk_subscriptions = subscriptions_by_name
		if self.target_panel == TargetPanels.PPA:
			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()
			}
		else: # TODO implement correct conflict resolution in case of target Plesk panel, just using PPA code is not enough
			plesk_sites = {}

		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()
		}

		ppa_existing_objects = ExistingObjectsModel(
			resellers=ppa_reseller_names,
			customers=ppa_usernames, 
			service_templates=ppa_service_templates,
			addon_service_templates=addon_service_templates,
			raw_ppa_subscriptions=raw_ppa_subscriptions,
			webspaces=ppa_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._get_session_file_path('ppa_existing_objects.yaml')
		write_yaml(target_filename, ppa_existing_objects)
		self.global_context.target_existing_objects = ppa_existing_objects
		return ppa_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("Unable to read migration list file.", exc_info=e)
				raise MigrationError(
					"There are non-UTF-8 characters in the migration list."
					" Convert the migration list file to UTF-8 encoding.")
			self.read_migration_list = True

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

		if self.global_context.migration_list_data is None:
			# 2. from source backups - 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(), ppa_webspace_names=[],
				ppa_service_templates={}, target_panel=self.target_panel
			)
			# 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("Migration tool has not enough information to transfer the subscriptions correctly. Please supply a transfer list.")
		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_plesk_backups()

	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 = self._get_migration_list_filename(options, migration_list_optional)

		if migration_list_file is None:
			return None

		try:
			with open_unicode_file(migration_list_file) as fileobj:
				mapping, errors = reader_func(fileobj)
		except (IOError, OSError) as e:
			logger.debug(u'Exception:', exc_info=e)
			raise MigrationError(
				u"Failed to read migration list file '%s': %s" % (migration_list_file, str(e))
			)

		if len(errors) > 0:
			raise MigrationError(
				u"There were some error(s) while reading migration list file. Please review and fix them: \n" + "\n".join(errors)
			)

		return mapping

	def _get_migration_list_filename(self, options, migration_list_optional=False):
		if options.migration_list_file is None:
			migration_list_file = self._get_session_file_path("migration-list")
			if not os.path.exists(migration_list_file): 
				if migration_list_optional:
					return None
				else:
					raise MigrationError(strip_multiline_spaces(u"""
						A migration list is not defined. Please either:
						(*) Put the correct migration list to '%s', or
						(*) Specify its location with "--migration-list-file" command-line option.

						The migration list defines the list of object (client accounts, domains) that should 
						be transferred from the source server. To generate the migration list sample, run the following command:
						# %s generate-migration-list config.ini 

						Please check the migration tool documentation for more details.
					""") % (
						migration_list_file,
						get_executable_file_name(),
						)
					)
		else:
			migration_list_file = options.migration_list_file

		logger.info(u"Migration list from '%s' is used", migration_list_file)
		return migration_list_file

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

	def _get_ppa_model(self):
		# if model does not exist - do not load it to make possible use safe in steps before 'convert'
		return self._load_ppa_model(False) if os.path.exists(self._get_session_file_path('ppa.yaml')) else None

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

		root_report = checking.Report(u"Detected problems", None)
		existing_objects = read_yaml(self._get_session_file_path('ppa_existing_objects.yaml'))

		target = self._convert_model(root_report, existing_objects, options)

		if not 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(
					u"Unable to continue migration until there are no issues at 'error' level in pre-migration checks. " +
					u"Please review pre-migration tree above and fix the errors. You can also use --ignore-pre-migration-errors command-line option if you're sure what you are doing."
				)

		if merge_pre_migration_report:
			# merge pre-migration check tree into final report tree
			safe = self._get_safe_lazy()
			for plesk_id, plesk_settings in self.source_plesks.iteritems():
				with closing(self.load_raw_plesk_backup(plesk_settings)) as backup:
					for subscription, report in self._iter_subscriptions_by_report_tree(backup, root_report.subtarget(u"Source server", plesk_id)):
						with log_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, ppa_existing_objects, options, soft_check=False):
		migration_list_data = self._read_migration_list_lazy(options)
		converted_resellers = self._convert_resellers(
			ppa_existing_objects.resellers,
			if_not_none(migration_list_data, lambda m: m.resellers), 
			root_report
		)
		if not soft_check:
			converted_resellers_filtered = [reseller for reseller in converted_resellers if reseller.source == 'ppa']
		else:
			converted_resellers_filtered = converted_resellers

		target_model = self._convert_accounts(
			converted_resellers_filtered, ppa_existing_objects, 
			migration_list_data.subscriptions_mapping, migration_list_data.customers_mapping,
			root_report, options
		)
		self._write_ppa_model(target_model)
		return target_model

	def _get_converter_class(self):
		return self._get_target_panel().get_converter_class()

	def _write_ppa_model(self, ppa_model):
		"""Store PPA 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
		"""
		write_yaml(self._get_session_file_path('ppa.yaml'), ppa_model)
		logger.debug(u"Saved panel objects to file 'ppa.yaml:")
		self.ppa_model = ppa_model 
		if self.safe is not None:
			self.safe.model = ppa_model

	def _load_ppa_model(self, apply_filtering=True):
		"""Either get PPA model from memory, or load it from file. See _write_ppa_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.ppa_model is None:
			try:
				self.ppa_model = read_yaml(self._get_session_file_path('ppa.yaml'))
			except IOError as err:
				if err.errno == errno.ENOENT:
					raise MigrationError(u"PPA model file not found, did you call 'convert' command?")
				else:
					raise

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

	def restore(self, options):
		self._check_connections(options)
		self._restore_impl(options)

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

		model = self._load_ppa_model()
		existing_objects = read_yaml(self._get_session_file_path('ppa_existing_objects.yaml'))

		safe = self._get_safe_lazy()
		self._get_importer(safe).import_clients_and_subscriptions(
			model, existing_objects, clean_up_vhost_skeleton=True, subscription_source_mail_ips=self._get_subscription_source_mail_ips()
		)

		self._finalize(finalize, options)

	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 _get_subscription_target_services(self, check_all_subscriptions_exist=True):
		filtered_model = self._load_ppa_model()
		target_panel = self._get_target_panel()

		subscription_infos = []
		for subscription in filtered_model.iter_all_subscriptions():
			plesk_server = target_panel.get_subscription_plesk_node(self.global_context, subscription.name)

			if self.target_panel == TargetPanels.PPA:
				hosting_request = plesk_ops.SubscriptionOperator.Dataset.HOSTING_BASIC
			else:
				hosting_request = plesk_ops.SubscriptionOperator.Dataset.HOSTING

			request = plesk_ops.SubscriptionOperator.Get(
				plesk_ops.SubscriptionOperator.FilterByName([subscription.name]),
				[
					plesk_ops.SubscriptionOperator.Dataset.GEN_INFO,
					hosting_request,
					plesk_ops.SubscriptionOperator.Dataset.MAIL,
				]
			)
			response = plesk_server.plesk_api().send(request)
			for result in response:
				if not hasattr(result, 'code') or result.code != 1013:
					subscription_infos.append(result.data[1])

		subscription_db_servers = {}
		for subscription in filtered_model.iter_all_subscriptions():
			plesk_server = target_panel.get_subscription_plesk_node(self.global_context, subscription.name)
			subscription_db_servers.update({
				subscription.name: result.data[1] for result in plesk_server.plesk_api().send(
					plesk_ops.SubscriptionOperator.DbServers.List(
						plesk_ops.SubscriptionOperator.FilterByName([subscription.name])
					)
				) if not hasattr(result, 'code') or result.code != 1013
			})

		subscription_target_services = {}
		for subscription_info in subscription_infos:
			name = subscription_info.gen_info.name
			plesk_server = target_panel.get_subscription_plesk_node(self.global_context, name)
			hosting = subscription_info.hosting
			mail = subscription_info.mail
			dns_ips = self._get_target_panel_api().get_domain_dns_server_ips(plesk_server, name)

			if self.target_panel == TargetPanels.PLESK and isinstance(hosting, SubscriptionHostingNone):
				# For Plesk, if subscription has no hosting - predict it
				# (see "target_panel.get_subscription_target_services" call below)
				continue

			db_server_group = {}
			for server in subscription_db_servers[name]:
				plesk_db_servers = plesk_server.get_db_servers()
				if server.server_id in plesk_db_servers:
					db_server_group[server.server_type] = plesk_db_servers[server.server_id]
				else:
					db_server_group[server.server_type] = None

			subscription_target_services[name] = SubscriptionTargetServices(
				web_ips=hosting.ip_addresses if isinstance(hosting, (
					plesk_ops.SubscriptionHostingVirtual,
					plesk_ops.SubscriptionHostingStandardForwarding,
					plesk_ops.SubscriptionHostingFrameForwarding
				)) else Ips(None, None),
				mail_ips=mail.ip_addresses if mail is not None else Ips(None, None),
				db_servers=db_server_group,
				dns_ips=[Ips(dns_ip, None) for dns_ip in dns_ips]
			)

		if check_all_subscriptions_exist:
			safe = self._get_safe_lazy()
			for subscription in self._load_ppa_model().iter_all_subscriptions():
				if subscription.name not in subscription_target_services:

					if self.target_panel == TargetPanels.PLESK:
						subscription_target_services[subscription.name] = target_panel.get_subscription_target_services(
							self.global_context,
							subscription.name
						)
						continue

					if self.target_panel == TargetPanels.PPA:
						solution = u"Check PPA task manager for failed tasks."
					else:
						solution = None

					safe.fail_subscription(
						subscription.name,
						u"Subscription does not exist on target panel or was not provisioned. "
						u"All transfer operations for that subscription will be skipped.",
						solution
					)

		return subscription_target_services

	def set_security_policy(self):
		self._changed_security_policy_nodes = set()
		for node in self._get_nodes_to_set_security_policy():
			try:
				signature, policy_on_node = self._get_security_policy(node)
				if policy_on_node != False:
					self._changed_security_policy_nodes.add(node)
					self._set_security_policy(node, signature, False)
			except Exception as e:
				logger.debug(u"Exception:", exc_info=e)
				logger.error(u"Failed to set security policy on some of target nodes: %s\n"
					u"Restoration of customers/users/resellers with weak passwords might be failed by Windows security policy.\n"
					u"Set 'PasswordComplexity' value to '0' with help of 'secedit' utility and re-run migration tool.\n"
					u"Restore required security policy after migration." % str(e)
				)

	def restore_security_policy(self):
			try:
				for node in self._changed_security_policy_nodes:
					try:
						signature, policy_on_node = self._get_security_policy(node)
						if not policy_on_node:
							self._set_security_policy(node, signature, True)
					except Exception as e:
						logger.debug(u"Exception:", exc_info=e)
						logger.error(strip_multiline_spaces("""
							Failed to restore security policy on target node %s: %s
							Restore 'PasswordComplexity' value with help of 'secedit' utility manually on the node
						""" % (node.description(), e,)))
			except Exception as e:
				logger.debug(u"Exception:", exc_info=e)
				logger.error(strip_multiline_spaces("""
					Failed to restore security policy on some of target nodes: %s
					Restore 'PasswordComplexity' value with help of 'secedit' utility manually 
					on all target Windows servers involved into migration.
				""" % (e,)))

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

	def _get_security_policy(self, node):
		with node.runner() as runner:
			# secedit required some file for export command. We create such file with 'type nul > filename' command 
			runner.sh('cmd.exe /C "type nul > c:\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 c:\security_policy.sdl' % (systemroot,))
			security_policy_content = runner.sh('cmd.exe /C "type c:\security_policy.sdl"')
			runner.sh_unchecked('cmd.exe /C "del c:\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

	def _set_security_policy(self, node, signature, required_policy):
		with node.runner() as runner:
			logger.debug(u"%s security policy on '%s'", "Enable" if required_policy else "Disable", node.description())
			policy_template = self._get_complexity_gpo_template(signature, required_policy)
			runner.upload_file_content('c:\security_policy.sdl', policy_template)
			runner.sh("secedit /validate c:\security_policy.sdl")
			runner.sh('cmd.exe /C "secedit /configure /db %systemroot%/Security/Database/secedit.sdb /cfg c:\security_policy.sdl"')
			runner.sh_unchecked('cmd.exe /C "del c:\security_policy.sdl"')

	def _get_complexity_gpo_template(self, 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(u"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 plesk_id, plesk_settings in self.source_plesks.iteritems():
			with log_context(plesk_id):
				with closing(self.load_raw_plesk_backup(plesk_settings)) as backup:
					for subscription_name in subscriptions_by_source[plesk_id]:
						for site_name in [subscription_name] + [site.name for site in backup.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(u"Restart IIS for IDN domains on %s.", node.description())
			with node.runner() as runner:
				runner.sh(u'iisreset')	

	def _sync_subscription_plan(self):
		"""In case of target Plesk we have 2 ways 
		controlled by 'transfer-resource-limits' option in config.ini:
		1) Transfer limits and permissions.
		2) Use limits and permissions from service plan.

		In the 2nd case we should keep subscriptions synced with service plans.
		But Plesk restore resets sync status of subscription when restoring 
		backup without plan reference (which is removed for other reasons
		at _remove_subscriptions_to_plans_relation function), making each 
		restored subscription locked. So we have to call sync in explicit way.
		(actually there is another way to resolve the problem 
		- fill Plesk backup with correct plan information, but that way is 
		a bit more complex)

		In the 1st case there is no sense to keep synced status, as limits
		and permissions of each subscription are taken from the source panel,
		not from new Plesk service template.

		For target PPA this is not applicable, as all subscriptions
		are "custom" in PPA Plesk panel (but not obligatory in POA).
		"""
		safe = self._get_safe_lazy()

		general_error_msg = "Failed to synchronize subscriptions with plans"
		with safe.try_general(general_error_msg, ''):
			if not self.target_panel == TargetPanels.PLESK:
				return

			transfer_resource_limits = is_transfer_resource_limits_enabled(
				self.global_context.config, 
				self.global_context.target_panel_obj
			)

			logger.debug("Sync subscriptions with plans")

			for subscription in self._iter_all_subscriptions():
				error_msg = "Failed to syncronize subscription with plan"
				with safe.try_subscription(subscription.name, error_msg):
					is_custom = subscription.model.plan_name is None
					is_locked = subscription.raw_backup.locked
					if not is_custom and (not is_locked or not transfer_resource_limits):
						self._get_target_panel_api().sync_subscription(
							subscription.name
						)

	# TODO remove
	def _is_mail_assimilated_subscription(self, subscription, subscription_target_services):
		"""
		subscription - instance of target_data_model.Subscription
		"""
		source_mail_server_settings = self._get_mail_plesks_settings()[self._get_mail_server_id(subscription.source)]
		source_mail_ip = source_mail_server_settings.ip

		target_mail_ips = subscription_target_services[subscription.name].mail_ips
		target_mail_ip = target_mail_ips.v4 or target_mail_ips.v6

		return source_mail_ip == target_mail_ip

	def _get_subscription_source_mail_ips(self):
		mail_ips = dict()

		for subscription in self._load_ppa_model().iter_all_subscriptions():
			source_mail_server_settings = self._get_mail_plesks_settings()[self._get_mail_server_id(subscription.source)]
			source_mail_ip = source_mail_server_settings.ip
			mail_ips[subscription.name] = source_mail_ip

		return mail_ips

	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_ppa_model()

		ppa_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, u"Failed to suspend subscription."):
					if subscription.source == 'ppa': 
						# Subscriptions that are already in PPA 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(u"Suspend PPA subscription '%s'", 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, u"Failed to suspend customer."):
					if client.source != 'ppa':
						# clients that are already on PPA should not be touched,
						# however their subscriptions that are not in PPA should be
						if not client.is_enabled:
							logger.debug(u"Suspend customer")
							self._get_target_panel_api().suspend_customer(ppa_client_ids[client.login])
					suspend_subscriptions(client.subscriptions)

		def suspend_resellers(resellers):
			for reseller in resellers:
				with safe.try_reseller(reseller.login, u"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())

		self._finalize(finalize, options)

	def _get_dns_server_hostname(self):
		dns_server_hostname = self.config.get('ppa', 'hostname')
		if dns_server_hostname == '':
			raise Exception(
				u"PPA NS server hostname must not be empty"
				u" (check 'hostname' parameter in [ppa] section of config file)"
			)
		return dns_server_hostname

	def _get_subscriptions_first_dns_info(self, subscription_name):
		"""Returns dict with keys 
		- 'ip_address' - IP address of the first DNS server
		- 'ns' - hostname of the first DNS server
		If there are no DNS servers related to the subscription - exception is raised
		"""
		dns_info = self.conn.target.poa_api().get_domain_name_servers(subscription_name)
		if len(dns_info) == 0:
			raise Exception(u"At least one DNS server should exist for subscription '%s'" % subscription_name)
		return dns_info[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 PPA if they exist in PPA
		existing_resellers = self._fetch_resellers_from_ppa(
			if_not_none(resellers_migration_list, lambda r: r.keys()) or []
		)
		# 3) convert resellers to PPA model
		report = checking.Report(u"Detected 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)
		self._get_importer(safe).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(
					u"Unable to continue migration until there are no issues at 'error' level in pre-migration checks. " +
					u"Please review pre-migration tree above and fix the errors. You can also use --ignore-pre-migration-errors command-line option if you're sure what you are doing."
				)

	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(u"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(u"******************** Summary ********************")
		logger.info(
			u"Operation finished successfully for %s out of %s resellers", 
			len(converted_resellers) - len(failed_resellers),
			len(converted_resellers)
		)

	def _resellers_exit_failed(self, failed_resellers):
		if len(failed_resellers) > 0:
			raise MigrationError(u"Failed to migrate some resellers. See log above for details")

	def _convert_resellers(self, existing_resellers, resellers_migration_list, report):
		return ResellersConverter().convert_resellers(
			self.global_context.source_server_infos, existing_resellers, resellers_migration_list,
			report, self.global_context.password_holder 
		)

	def _convert_accounts(self, converted_resellers, existing_objects, subscriptions_mapping, customers_mapping, report, options):
		converter = self._get_converter_class()(self.conn.target.panel_admin_password, existing_objects, options, self.multiple_webspaces)
		converter.set_ip_mapping(self.global_context.ip_mapping)
		plain_report = checking.PlainReport(report, *self._extract_source_objects_info())
		converter.convert_plesks(
			self.global_context.source_server_infos, plain_report,
			subscriptions_mapping, customers_mapping, 
			converted_resellers,
			self.global_context.password_holder,
			custom_subscriptions_allowed=self._get_target_panel().has_custom_subscriptions_feature()
		)
		converter.fix_emails_of_aux_users()
		return converter.get_ppa_model()

	def _fetch_resellers_from_ppa(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(u"Resource limits transfer is available only when migrating from Parallels H-Sphere")

	def transfer_billing(self, options):
		raise MigrationError(u"Billing data migration is available only when migrating from Parallels H-Sphere")

	def check_billing(self, options):
		raise MigrationError(u"Billing data migration is available only when migrating from Parallels H-Sphere")

	def compare_payments(self, options):
		raise MigrationError(u"Estimated payments comparison is available only when migrating from Parallels H-Sphere")

	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(u"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(u"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="Transferred Domains' Functional Issues",
			checks_source=checks_source,
		)

	def _test_services(self, options):
		self.global_context.test_services_report = checking.Report(
			u"Services' 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,
			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=u"Transferred Web Sites' Issues",
			checks_source=self._get_web_hosting_checks_data_source(),
		)

	def _test_dns(self, options):
		self._run_hosting_checks(
			report_title=u"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=u"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=u"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,
			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):
		self._finalize_test(
			self.global_context.post_migration_check_report,
			report_filename,
			print_backup_hosting_report
		)

	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):
		print_hosting_report(
			report, 
			save_report_function=lambda contents: migrator_utils.save_report(
				contents, self._get_session_file_path(report_file_name)
			)
		)

	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(u"Exception:", exc_info=e)
			logger.error(u"Unable to save external report data into file '%s': %s" % (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(backup, subscription):
			return HostingObjectWebSubscription(
				backup, subscription, self._create_migrated_subscription
			)

		return ServerBackupsCheckSource(
			{server_id: backup for server_id, backup in self.iter_plesk_backups()},
			create_check_subscription_class
		)

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

		return ServerBackupsCheckSource(
			{server_id: backup for server_id, backup in self.iter_converted_plesk_backups()},
			create_check_subscription_class
		)

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

		return ServerBackupsCheckSource(
			{server_id: backup for server_id, backup in self.iter_converted_plesk_backups()},
			create_check_subscription_class
		)

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

		return ServerBackupsCheckSource(
			{server_id: backup for server_id, backup in self.iter_converted_plesk_backups()},
			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()
					)
				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):
				backup = self.load_raw_plesk_backup(self.source_plesks[source_id])
				backup_subscription = backup.get_subscription(subscription_name)
				users = []
				for user in itertools.chain(
					backup_subscription.get_database_users(db_name, db_type),
					backup_subscription.get_overall_database_users(db_type)
				):
					users.append(User(user.name, user.password, user.password_type))
				return users

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

		return ServerBackupsCheckSource(
			{server_id: backup for server_id, backup in self.iter_plesk_backups()},
			create_check_subscription_class
		)

	def _iter_all_subscriptions(self):
		ppa_model = self._load_ppa_model()
		for model_subscription in ppa_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_plesk_backup(source_settings)) as raw_backup:
			raw_subscription_from_backup = raw_backup.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)

		return Ips(None, None) 

	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, plesk_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_plesks[plesk_id].is_windows

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

		unfiltered_model = self._load_ppa_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(u"All subscriptions are failed to 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(
						u"Failed to migrate some objects. See log above for details")

	def _print_summary(self, options):
		unfiltered_model = self._load_ppa_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(u"******************** Summary ********************")
		logger.info(
			u"Operation finished successfully for %s out of %s subscriptions\n"
			u"To verify that the transfer completed correctly, run 'panel-migrator test-all config.ini'.\n"
			u"This command will check the operation of web, mail, DNS, database, "
			u"FTP/SSH/RDP services for each transferred domain.",
			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 = [
			(u"subscription(s)", safe.failed_objects.subscriptions, quote), 
			(u"reseller(s)", safe.failed_objects.resellers, quote), 
			(u"client(s)", safe.failed_objects.clients, quote), 
			(u"plan(s)", safe.failed_objects.plans, lambda p: (
				u"'%s' owned by %s" % (
					p[1], 
					u"reseller '%s'" % p[0] if p[0] is not None else 'admin'
				)
			)),
			(u"auxiliary user(s)", safe.failed_objects.auxiliary_users, lambda a: (
				u"'%s' owned by customer '%s'" % (a[1], a[0],)
			)),
			(u"auxiliary user role(s)", safe.failed_objects.auxiliary_user_roles, lambda a: (
				u"'%s' owned by customer '%s'" % (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(
					u"Failed to perform operation on %s %s: %s",
					len(error_description), name, u', '.join(error_description))

		if len(safe.failed_objects.subscriptions) > 0 and self.target_panel not in (TargetPanels.PPA_WEB_SN, TargetPanels.PVPS):
			suffix = time.time()
			ppa_existing_objects = read_yaml(self._get_session_file_path('ppa_existing_objects.yaml'))
			failed_subscriptions_filename = self._get_session_file_path("failed-subscriptions.%s" % (suffix,))
			successful_subscriptions_filename = self._get_session_file_path("successful-subscriptions.%s" % (suffix,))

			ppa_service_templates = { None: [ st.name for st in ppa_existing_objects.service_templates if st.owner_id == poa_api.Identifiers.OID_ADMIN ] }
			for r in ppa_existing_objects.resellers.itervalues():
				ppa_service_templates[r.contact.username] = [ st.name for st in ppa_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(u"Exception message: %s" % (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 ppa_existing_objects.webspaces ],
				ppa_service_templates,
				self.target_panel,
				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 ppa_existing_objects.webspaces ],
				ppa_service_templates,
				self.target_panel,
				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(
				u"%s of %s subscription(s) failed to migrate. " + 
				u"All of them are listed in \"%s\".\n" + 
				u"To repeat migration for these subscriptions only, run migrator in the following way:\n" + 
				u"# %s %s config.ini --migration-list-file %s", 
				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:
			return u"%s\nException: %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(u"Detailed Migration Status", None)

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

		for plan_name in unfiltered_model.plans:
			plan_report = root_report.subtarget(u"Plan", plan_name)
			if (None, plan_name) in safe.failed_objects.plans:
				self._add_report_issues(
					plan_report,
					safe.failed_objects.plans[(None, plan_name)]
				)
		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):
		ppa_model = self._load_ppa_model()
		subscriptions_by_source = defaultdict(list)
		for subscription in ppa_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):
		report = 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
		if report_file_name is not None:
			path = migrator_utils.save_report(report, self._get_session_file_path(report_file_name))
			logger.info("The report was saved into the 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 _copy_db_content(self):
		for subscription in self._iter_all_subscriptions():
			self._copy_single_subscription_db_content(subscription)

	def _copy_single_subscription_db_content(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_single_db_content(db_info)

	def _copy_single_db_content(self, db_info):
		"""
		:type db_info: parallels.common.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
					u"Failed to copy content of database '%s': migration of databases from Windows to Unix is not allowed" % (
						db_info.database_name),
					solution=u"Fix service template subscription is assigned to, so databases are provisioned to Windows servers",
					is_critical=False
				)
			else:
				repeat_error = u"Unable to copy content of database '%s' on subscription '%s'. Trying to copy content again." % (
					db_info.database_name, db_info.subscription_name)

				def _repeat_copy_windows_content():
					database_utils.copy_db_content_windows(db_info)

				safe.try_subscription_with_rerun(
					_repeat_copy_windows_content,
					db_info.subscription_name,
					u"Failed to copy content of 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
					u"Failed to copy content of database '%s': migration of databases from Unix to Windows is not allowed" % (
						db_info.database_name),
					solution=u"Fix service template subscription is assigned to, so databases are provisioned to Unix servers",
					is_critical=False
				)
			else:
				repeat_error = u"Unable to copy content of database '%s' on subscription '%s'. Trying to copy content again." % (
					db_info.database_name, db_info.subscription_name
				)

				def _repeat_copy_linux_content():
					database_utils.copy_db_content_linux(
						db_info,
						self.global_context.ssh_key_pool.get(
							db_info.source_database_server, db_info.target_database_server
						).key_pathname
					)

				safe.try_subscription_with_rerun(
					_repeat_copy_linux_content,
					db_info.subscription_name,
					u"Failed to copy content of database '%s'" % 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.common.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.database_name not in target_databases:
				safe.fail_subscription(
					db_info.subscription_name,
					"%s database '%s' does not exist on target. Its content will not be copied." % (
						db_info.target_database_server.type_description, db_info.database_name
					),
					solution=(
						"Resolve the issue with database creation or create database on target panel, "
						"then run copy-db-content command for that subscription to copy database contents."
					)
				)
				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("Exception: ", exc_info=True)
			logger.error(
				"Failed to detect list of databases on %s: %s", target_database_server.description(), str(e)
			)
			return None

	def _list_databases_to_copy(self):
		"""
		:rtype list[parallels.common.migrator.DatabaseInfo]:
		"""
		ppa_model = self._load_ppa_model()

		safe = self._get_safe_lazy()
		clients = ppa_model.clients.values() + sum((r.clients for r in ppa_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_server_id, subscriptions in servers_subscriptions.iteritems():
			with closing(self.load_raw_plesk_backup(self.source_plesks[source_server_id])) as backup:
				for subscription in subscriptions:
					with safe.try_subscription(subscription.name, u"Failed to detect databases of subscription."):
						target_db_nodes = self._get_subscription_nodes(subscription.name).database

						backup_subscription = backup.get_subscription(subscription.name)
						for db in backup_subscription.all_databases:
							source_database_server_model = self._get_src_db_server(db, backup)

							target_database_server = target_db_nodes.get(db.dbtype)
							if target_database_server is None:
								raise Exception(
									u"Database '{db.name}' from {db.dbtype}"
									u" server at {db.host}:{db.port} will not"
									u" be copied as no {db.dbtype} DB server"
									u" is assigned to subscription"
									u" '{subscr}'".format(db=db, subscr=subscription.name)
								)

							target_database_subscription = get_database_subscription(
								self._create_migrated_subscription(subscription.name).panel_target_server,
								db.name, db.dbtype, target_database_server.host()
							)

							if (
								target_database_subscription is not None and
								target_database_subscription != subscription.name
							):
								logger.debug(
									u"Skip copy database '%s' from %s server as it conflicts"
									u" with another database on target",
									db.name, db.dbtype
								)
								continue

							def _compare_mssql_hosts(_source_host, _target_host, _target_ip):
								if _source_host == 'localhost' or _target_host == 'localhost':
									return False
								if _source_host == _target_host:
									return True
								# host of non local MSSQL sever should have form like <host>\<service>,
								# so split it apart and compare separately
								source_host_parts = _source_host.split('\\')
								target_host_parts = _target_host.split('\\')
								if len(source_host_parts) == 1 and len(target_host_parts) == 1:
									# hostname of source server should resolves into target server IP-address
									return _target_ip in resolve_all(source_host_parts[0])
								if len(source_host_parts) == 2 and len(target_host_parts) == 2:
									# name of MSSQL service should be the same
									if source_host_parts[1] != target_host_parts[1]:
										return False
									# hostname of source server should resolves into target server IP-address
									return _target_ip in resolve_all(source_host_parts[0])
								return False

							if db.dbtype == 'mssql':
								same_host = _compare_mssql_hosts(
									source_database_server_model.host,
									target_database_server.host(),
									target_database_server.ip()
								)
							else:
								if source_database_server_model.host == 'localhost':
									source_host = self.source_plesks[source_server_id].ip
								else:
									source_host = source_database_server_model.host
								same_host = target_database_server.ip() in resolve_all(source_host)

							same_port = any([
								source_database_server_model.port == target_database_server.port(),  # exact match
								db.dbtype == 'mssql' and
								source_database_server_model.port in (None, 0, 1433) and
								target_database_server.port() in (None, 0, 1433)  # for MSSQL 0 and 1433 mean the same 1433
							])

							# 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_port and same_host:
								logger.debug(
									u"Database '{db.name}' from {db.dbtype} server at {db.host}:{db.port} remains on "
									u"the same server, content will not be copied".format(db=db))
								continue

							if target_database_server.is_external() and db.dbtype != 'mssql':
								errormessage = (
									u"Only migration to external MSSQL database server is supported, migration to "
									u"other types of external database servers is not supported. Database '{db.name}' "
									u"cannot be migrated to the database server {target_host}:{target_port}".format(
										db=db,
										target_host=target_database_server.host(),
										target_port=target_database_server.port()
									)
								)
								safe.fail_subscription(subscription.name, errormessage)
								continue

							source_panel_server = self.conn.get_source_node(source_server_id)

							databases.append(DatabaseInfo(
								source_database_server=source_panel_server.get_database_server(
									source_database_server_model,
									self.external_db_servers
								),
								target_database_server=target_database_server,
								subscription_name=subscription.name,
								database_name=db.name
							))

		return databases

	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_backup.iter_domains():
				if domain.name == domain_name:
					return subscription._name

		raise Exception(
			"Failed to find domain '%s' in converted backup" % domain_name
		)

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

	def _check_target_licenses(self, options):
		"""Check licenses on target PPA cluster - on management and on service nodes"""
		if self.target_panel == TargetPanels.PPA:
			if not options.skip_license_checks:
				self._check_ppa_license()
				self.check_nodes_licenses()

	def _check_ppa_license(self):
		if self.conn.target.poa_api().get_ppa_license_status() != "licensed":
			raise MigrationError(u"PPA license is missing or invalid, please fix")

	def check_nodes_licenses(self):
		self._check_nodes_licenses()

	def _check_nodes_licenses(self):
		"""Check if Plesk licenses are assigned to PPA nodes
		"""
		try:
			licensing_schema = if_not_none(
				self.conn.target.plesk_api().send(plesk_ops.ServerOperator.Get(dataset=[plesk_ops.ServerOperator.Dataset.KEY])).data.key,
				lambda key: key.properties.get('licensing-scheme')
			)
			logger.debug(u"PPA licensing schema is: %s", licensing_schema)
			# in "websites" mode PPA does not require a license for each service node, only one main license is necessary, and there is a limit of webspaces you can create
			# in "nodes" mode PPA requires licenses for each Web and Mail node
			if licensing_schema == 'websites': 
				logger.info(u"PPA licenses are ok")
				return # no need to perform checks for each of service nodes

			service_nodes_info = self.conn.target.poa_api().get_service_nodes_info()

			unlicensed_services = set(['dns', 'db']) # DNS and all database nodes do not require any license to work
			node_services = {node_info['host_id']: set(node_info['node_services']) for node_info in self.conn.target.poa_api().msp_license_manager_get_sip_data()}

			for service_node_info in service_nodes_info:
				if all([
					service_node_info.is_ready_to_provide,
					service_node_info.license_status != 'licensed',
					not node_services[service_node_info.host_id] <= unlicensed_services 
				]):
					raise LicenseValidationError(u"There is no Plesk license at %s, please fix" % self.conn.target.get_ppa_node_description(service_node_info.host_id))
			logger.info(u"Plesk licenses on ready to provide PPA service nodes are ok")
		except LicenseValidationError:
			raise
		except Exception as e:
			logger.debug(u"Exception:", exc_info=e)
			logger.error(u"Failed to get information about PPA main license key: %s. License pre-migration checks will be skipped." % e)

	def _refresh_service_node_components_for_windows(self):
		"""Refresh components list of Windows service nodes in PPA 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 Windows service node
		(there is no way to do this with the help of Plesk/PPA, only manual way)
		- 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.
		Actually there is no way in PPA to update component list from GUI,
		only service_node command line utility can handle this.
		"""
		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(
						u"Failed to refresh components list for %s: %s. See debug.log for more details.", 
						web_node.description(), e
					)
					logger.debug(u"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(
				u"Failed to refresh components list for Windows service nodes: %s. See debug.log for more details.", e
			)
			logger.debug(u"Exception:", exc_info=e)

	def _get_subscription_windows_web_nodes(self):
		web_nodes = []
		for subscription in self._load_ppa_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._load_ppa_model().iter_all_subscriptions():
			subscription_mssql_node = self._get_subscription_nodes(subscription.name).database.get('mssql')
			if subscription_mssql_node is not None and not subscription_mssql_node.is_external():
				subscription_node = subscription_mssql_node.get_subscription_node()
				if subscription_node not in mssql_nodes:
					mssql_nodes.append(subscription_node)
		return mssql_nodes

	def remove_webspace(self, options):
		self._check_connections(options)
		try:
			webspace_id = int(options.webspace_id)
		except ValueError:
			logger.debug(u"Exception:", exc_info=True)
			raise MigrationError(u"Webspace ID specified by '--webspace-id' option should be valid integer")
		
		try:
			self.conn.target.poa_api().remove_webspace(webspace_id)
		except poa_api.PoaApiError as e:
			logger.debug(u"Exception:", exc_info=True)
			raise MigrationError(u"Failed to remove the webspace: %s" % (str(e),))

		logger.info(u"Webspace remove has been scheduled. Please check PPA task manager and wait till all related tasks are complete")

	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 node (PPA management node or target Plesk 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("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_ppa_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(
				u"Failed to check disk space requirements for %s: %s. Check debug.log for more details. Still migration will proceed to the next steps.", 
				self.conn.target.main_node_description(), e
			)
			logger.debug(u'Exception:', exc_info=e)

		if not check_success:
			raise MigrationNoContextError((
				u"%s has insufficient free disk space for migration. "
				u"Migration is stopped. Free some disk space, then run 'transfer-accounts' command again. " + 
				(u"To skip disk space requirements, check for %s pass --skip-main-node-disk-space-checks command line option" if show_skip_message else u"")
				) % (
					self.conn.target.main_node_description(), 
					self.conn.target.main_node_description()
				)
			)
		else:
			logger.info("Disk space requirements for %s are met", self.conn.target.main_node_description())

	def check_infrastructure(self, options, show_skip_message=True):
		"""Perform infrastructure checks before trying 
		to restore subscriptions' hosting settings, 
		restoring web/mail/database content and so on. 

		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, 
							"Internal error: %s.\nMigration tool will skip corresponding infrastructure checks and proceed to the next migration steps." % (error_message)
						), u""
					)
					logger.error(u"%s Exception message: %s", error_message, exception)

				try:
					yield
				except run_command.HclRunnerException as e:
					logger.debug(u"Exception:", exc_info=True)
					if e.cause is not None and isinstance(e.cause, poa_utils.HclPleskdCtlNonZeroExitCodeError):
						logger.debug(u"Exception:", exc_info=True)
						report.add_issue(
							checking.Problem(
								'infrastructure_checks_service_node_unavailable', checking.Problem.ERROR, 
								"Errors occurred while connecting to %s." % (e.host_description)
							), 
							"Please check that:\n"
							"* PPA service node is running.\n"
							"* The pem service is running on the node. To start the service on a Linux node, run '/etc/init.d/pem start', on a Windows node, run 'net start pem'.\n"
							"* There are no firewall rules that block communication between the PPA management node and that service node.\n"
							"* Errors might be caused by a temporary network issue. In such a case, just re-run the migration tool."
						)
					else:
						report_internal_error(e)
				except Exception as e:
					logger.debug(u"Exception:", exc_info=True)
					report_internal_error(e)

			report = checking.Report(u"Infrastructure", None)
			with safe(report, "Failed to check connections."):
				self._check_infrastructure_connections(report, safe)
			with safe(report, "Failed to check disk space requirements."):
				self._check_disk_space(report, safe)
			with safe(report, "Failed to check connections to 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, "Failed to 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(u"Failed to perform infrastructure checks: %s. Check debug.log for more details. Still migration will proceed to the next steps.", e)
			logger.debug(u'Exception:', exc_info=e)

		if not infrastructure_check_success:
			raise MigrationNoContextError(
				u"There are infrastructure issues that should be fixed prior to the next migration steps. "
				u"Migration is stopped, please check and fix the issues listed above and then run transfer-accounts command again. " +
				(u"If you want to skip infrastructure checks pass --skip-infrastructure-checks command line option" if show_skip_message else u"")
			)

	def _check_infrastructure_connections(self, report, safe):
		logger.info(u"Check connection requirements")
		checks = infrastructure_checks.InfrastructureChecks()

		web_report = report.subtarget(u"Connections between source and the destination web server nodes", None)
		with safe(web_report, "Failed to check connections between source and the destination web server nodes"):
			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(u"Connections between source and the destination mail server nodes", None)
		with safe(mail_report, "Failed to check connections between source and the destination mail server nodes"):
			self._check_unix_copy_mail_content_rsync_connections(checks, mail_report)
			self._check_windows_copy_mail_content(checks, mail_report)

		db_report = report.subtarget(u"Connections between source and the destination database server nodes", None)
		with safe(db_report, "Failed to check connections between source and the destination database server nodes"):
			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(u"Check disk space requirements")
		disk_space_report = report.subtarget(u"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)))
		mysql_databases_by_source_nodes = 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)
			mysql_databases_by_source_nodes[nodes.source].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():
				source_node_info = infrastructure_checks.UnixDiskSpaceSourcePleskNode(
					node=source_node,
					web_domains=content_info.get('web', []),
					mail_domains=content_info.get('mail', []),
					mysql_databases=content_info.get('mysql_databases', [])
				)
				source_nodes.append(source_node_info)

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

		checker_source = infrastructure_checks.UnixSourceSessionDirDiskSpaceChecker()
		for source_node, mysql_databases in mysql_databases_by_source_nodes.iteritems():
			checker_source.check(
				source_node, mysql_databases=mysql_databases, report=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()

		disk_usage_script_name = 'folder_disk_usage.vbs'
		mssql_disk_usage_script = 'mssql_disk_usage.ps1'

		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,
				du_local_script_path=migrator_utils.get_package_scripts_file_path(
					parallels.plesks_migrator, disk_usage_script_name
				),
				mssql_disk_usage_local_script_path=migrator_utils.get_package_scripts_file_path(
					parallels.plesks_migrator, mssql_disk_usage_script
				),
				report=report
			)

		mysql_databases_by_source_nodes = defaultdict(list)
		for nodes, databases in mysql_databases.iteritems():
			mysql_databases_by_source_nodes[nodes.source].extend(databases)

		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 = migrator_utils.get_package_scripts_file_path(
				parallels.plesks_migrator, 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, node_type_name, nodes_pair_info):
		"""
		Arguments
			checker - instance of UnixFileCopyBetweenNodesChecker
		"""
		filename = 'check-rsync-testfile-%s' % (node_type_name,)
		local_temp_filepath=self._get_session_file_path(filename)
		return checker.check(nodes_pair_info, filename, local_temp_filepath, 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(),
				node_type_name='web',
				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(),
				node_type_name='mail',
				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(),
				node_type_name='db',
				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
		"""
		filename = 'check-rsync-testfile-windows-web'
		local_temp_filepath = self._get_session_file_path(filename)
		return checker.check(
			nodes_pair_info,
			filename,
			local_temp_filepath,
			get_rsync=self._get_rsync
		)

	def _check_mysql_max_allowed_packet(self, report):
		logger.info(u"Check MySQL max allowed packet")
		mysql_report = report.subtarget(u"Check MySQL max allowed packet", 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.plesks_migrator.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_ppa_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_path_to_raw_plesk_backup(self, plesk_id):
		return self.session_files.get_path_to_raw_plesk_backup(plesk_id)

	def get_path_to_raw_plesk_backup_by_settings(self, settings):
		return self.session_files.get_path_to_raw_plesk_backup(settings.id)

	def get_path_to_converted_plesk_backup(self, plesk_id):
		return self.session_files.get_path_to_converted_plesk_backup(plesk_id)

	@cached
	def load_shallow_plesk_backup(self, plesk_settings):
		if self.shallow_dump_supported(plesk_settings.id):
			session_files = self.global_context.session_files
			backup_filename = session_files.get_path_to_shallow_plesk_backup(
				plesk_settings.id
			)
			if os.path.exists(backup_filename):
				return plesk_backup_xml.load_backup(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_plesk_backup(plesk_settings)
		else:
			return self.load_raw_plesk_backup(plesk_settings)

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

	@cached
	def load_raw_plesk_backup(self, plesk_settings):
		backup_filename = self.get_path_to_raw_plesk_backup_by_settings(plesk_settings)
		return plesk_backup_xml.load_backup(
			backup_filename,
			self.global_context.migration_list_data,
			is_expand_mode=self.is_expand_mode(),
			discard_mailsystem=self._is_mail_centralized(plesk_settings.id),
		)

	@cached
	def load_converted_plesk_backup(self, plesk_settings):
		backup_filename = self.get_path_to_converted_plesk_backup(plesk_settings.id)
		return plesk_backup_xml.load_backup(
			backup_filename, 
			self.global_context.migration_list_data,
			is_expand_mode=self.is_expand_mode(),
			discard_mailsystem=self._is_mail_centralized(plesk_settings.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 plesk_id in self.source_plesks.iterkeys():
			yield plesk_id, self.get_path_to_raw_plesk_backup(plesk_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 plesk_id in self.source_plesks.iterkeys():
			yield plesk_id, self.get_path_to_converted_plesk_backup(plesk_id)

	@staticmethod
	def is_expand_mode():
		return False

	def _is_mail_centralized(self, plesk_id):
		return False

	def _get_source_mail_node(self, subscription_name):
		subscription = find_only(
			self._load_ppa_model().iter_all_subscriptions(), 
			lambda s: s.name == subscription_name,
			error_message="Failed to 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_ppa_model().iter_all_subscriptions(), 
			lambda s: s.name == subscription_name,
			error_message="Failed to find subscription by name"
		)
		source_node_id = subscription.source
		return self._get_source_node(source_node_id)

	@cached
	def _get_source_node(self, node_id):
		node_settings = self._get_source_servers()[node_id]
		return SourceServer(node_id, node_settings, self._get_migrator_server())

	def _get_source_dns_ips(self, plesk_id):
		return [self.source_plesks[plesk_id].ip]

	def _get_mail_plesks_settings(self):
		"""Overridden in Expand migrator"""
		return self.source_plesks

	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_plesks[sub.source].ip

	def iter_shallow_plesk_backups(self):
		for plesk_id, plesk_settings in self.source_plesks.iteritems():
			with closing(self.load_shallow_plesk_backup(plesk_settings)) as backup:
				yield plesk_id, backup

	def iter_plesk_backups(self):
		for plesk_id, plesk_settings in self.source_plesks.iteritems():
			with closing(self.load_raw_plesk_backup(plesk_settings)) as backup:
				yield plesk_id, backup

	def iter_converted_plesk_backups(self):
		for plesk_id, plesk_settings in self.source_plesks.iteritems():
			with closing(self.load_converted_plesk_backup(plesk_settings)) as backup:
				yield plesk_id, backup

	def _get_plesk_infos(self, sources=None):
		class PleskInfo(namedtuple('PleskInfo', ('id', 'settings'))):
			def load_raw_backup(self_):
				backup = self.load_raw_plesk_backup(self_.settings)
				return backup

		source_plesks = sources or self.source_plesks
		return [
			PleskInfo(
				id=id, 
				settings=settings, 
			)
			for id, settings in source_plesks.iteritems()
		]

	@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_plesks.keys()}
		for plesk_id, plesk_backup in self.iter_plesk_backups():
			for subscription in plesk_backup.iter_admin_subscriptions():
				subscriptions[plesk_id][subscription.name] = []
			for client in plesk_backup.iter_clients():
				customers[plesk_id][client.login] = []
				for subscription in client.subscriptions:
					subscriptions[plesk_id][subscription.name] = [obj(type='Client', name=client.login)]

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

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