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.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.target_panel_ppa.panel import PPATargetPanel
from parallels.target_panel_plesk.panel import PleskTargetPanel
from parallels.common.connections_config import MailContent
from parallels.common.connections_config import is_transfer_resource_limits_enabled
from parallels.common import MigrationNoContextError
from parallels.common.context import log_context
from parallels.common.utils import plesk_utils
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
from parallels.common.plesk_backup.plesk_backup_xml import SubscriptionNotFoundException
from parallels.target_panel_ppa.import_api.import_api import \
		PPAImportAPI, WebspacesRequestsHolder
from parallels.target_panel_plesk.import_api.import_api_unix import \
		PleskUnixImportAPI
from parallels.target_panel_plesk.import_api.import_api_windows import \
		PleskWindowsImportAPI
from parallels import poa_api
from parallels.plesks_migrator.migration_list import MigrationList
from .importer import Importer
from parallels.common.utils import ssh_utils
from parallels.common.utils.yaml_utils import write_yaml, read_yaml, pretty_yaml
from . import checking
import parallels.common.restore_hosting
from .existing_objects_model import ExistingObjectsModel
from parallels.common.utils.migrator_utils import trace
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 import windows_utils
from parallels.common.config_utils import ConfigSection, config_get
from parallels.common.utils import migrator_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.utils.windows_utils import path_join as windows_path_join
from parallels.common.target_panels import TargetPanels
from parallels.common.converter.business_objects.plans import PlansConverter
from parallels.common.migrated_subscription import MigrationSubscription
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
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.target_panel_ppa.converter.converter import PPAConverter
from parallels.target_panel_plesk.converter.converter import PleskConverter
from parallels.common.converter.business_objects.resellers import ResellersConverter
from parallels.common.workflow.runner import ActionRunner
from . import MigrationError, run_and_check_local_command
from .safe import Safe


class LicenseValidationError(MigrationError):
	pass

MailServer = namedtuple('MailServer', ('ip', 'ssh_auth'))
DatabaseToCopy = namedtuple('DatabaseToCopy', (
	'plesk_id', # source server ID
	'subscription_name',
	'target_node', # instance of DatabaseTargetServer
	'src_server_access',
	'src_dump_filename',
	'src_db_server',
	'dst_db_server',
	'db_name',
))

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.external_db_servers = {}
		self.source_plesks = self.conn.get_information_servers()
		self.session_files = self._create_session_files()
		self.global_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.windows_rsync=self._windows_rsync
		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.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.migrator = self
		self.global_context.config = config
		self.global_context.migration_list_data = None

		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 = ActionRunner(self.global_context)
		self.workflow = self._create_workflow()

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

	@contextmanager
	def _windows_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 is_ipv4(webmail_ipv4) == False:
			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:
			return PPATargetPanel()
		elif self.target_panel == TargetPanels.PLESK:
			return PleskTargetPanel()

	def _get_target_panel_api(self):
		if self.target_panel == TargetPanels.PPA:
			return PPAImportAPI(self.conn.target, WebspacesRequestsHolder(self._get_session_file_path('webspaces_requests.yaml')))
		elif self.target_panel == TargetPanels.PLESK:
			if self.conn.target.is_windows:
				return PleskWindowsImportAPI(self.conn.target)
			else:
				return PleskUnixImportAPI(self.conn.target)

	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'

		if self.target_panel == TargetPanels.PPA:
			default = 'none'
			section = 'ppa'
		else:
			default = '1800'
			section = 'plesk'

		target_section = ConfigSection(self.config, section)
		self.apache_restart_interval = 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
		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
			"""))

	def _load_connections_configuration(self):
		return Connections(self.config, self.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.conn.target, 
			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:
				with migrator_utils.plain_passwords_enabled(runner): # this is necessary to get destination database server password
					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 _version_to_tuple(self, v):
		return tuple(map(int, (v.split("."))))

	def _check_target(self):
		try:
			logger.debug(u"Check connections to target nodes")
			self.conn.target.check_connections()
			logger.debug(u"Check Plesk version of the main target node")
			plesk_version = plesk_api_utils.get_plesk_version(self.conn.target.plesk_api())

			if self._version_to_tuple(plesk_version) < self._version_to_tuple('11.5'):
				raise MigrationError(strip_multiline_spaces(u"""
					You are trying to migrate to old %s version '%s'.
					(*) If you want to migrate to PPA 11.1.x please use previous version of migration tools: http://autoinstall.plesk.com/PPA_11.1.0/migration-tools/
					(*) If you want to migrate to Plesk < 11.5 please use migration tools bundled with Plesk""") % (
						self._get_target_panel().name, plesk_version
					) 
				)
			if not self.conn.target.is_windows:
				logger.debug(u"Check log priority on target Plesk node")
				self._check_plesk_log_priority()
		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._run_hosting_checks(
				"", report_title=u"Services' Issues",
				checks_source=ServiceCheckSource(
					self._get_target_panel().get_service_nodes(self.conn.target),
					self.conn.target
		                ),
				report_filename="test_service_report",
				print_hosting_report=print_service_hosting_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. Still migration will proceed to the next steps.", e)

	def _check_plesk_log_priority(self):
		""" This function checks 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

		with self.conn.target.main_node_runner() as runner:
			plesk_root = plesk_utils.get_unix_product_root_dir(runner)
			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 MigrationError(
						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
		self._read_migration_list_lazy(options)

		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_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


		# 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 PPA 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 PPA 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 PPA 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 PPA Plesk 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 PPA 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 PPA Plesk 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,
		)

		# XXX existing objects should not be stored into 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):
		if not self.read_migration_list:
			self.global_context.migration_list_data = self._read_migration_list(options)
			self.read_migration_list = True

			self.load_raw_plesk_backup.clear()	# 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.

		return self.global_context.migration_list_data

	def _get_source_data(self):
		return lambda: self.iter_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_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 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._check_updates()
		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(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(self, root_report, ppa_existing_objects, options):
		return self._convert_model(root_report, ppa_existing_objects, options)

	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

		result = 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(result)
		return result

	def _get_converter_class(self):
		if self.target_panel == TargetPanels.PPA:
			return PPAConverter
		elif self.target_panel == TargetPanels.PLESK:
			return PleskConverter

	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)
		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):
		unfiltered_model = self._load_ppa_model(apply_filtering=False)	# retrieve info even for the failed subscriptions, to avoid problems. TODO: clarify that migrator never needs info on failed subscriptions, and load filtered model here instead.

		subscriptions_by_id = {}
		for subscription_names_chunk in partition_list([ s.name for s in unfiltered_model.iter_all_subscriptions() ], chunk_size=1):
			subscriptions_by_id.update({
				result.data[0]: 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,
							plesk_ops.SubscriptionOperator.Dataset.HOSTING_BASIC if self.target_panel == TargetPanels.PPA else plesk_ops.SubscriptionOperator.Dataset.HOSTING,
							plesk_ops.SubscriptionOperator.Dataset.MAIL,
						]
					)
				) if not hasattr(result, 'code') or result.code != 1013
			})

		subscription_db_servers = {}
		for subscription_names_chunk in partition_list([ s.name for s in unfiltered_model.iter_all_subscriptions() ], chunk_size=1):
			subscription_db_servers.update({
				result.data[0]: result.data[1] for result in self.conn.target.plesk_api().send(
					plesk_ops.SubscriptionOperator.DbServers.List(
						plesk_ops.SubscriptionOperator.FilterByName(subscription_names_chunk)
					)
				) if not hasattr(result, 'code') or result.code != 1013
			})

		db_servers = {
			result.data.id : result.data
			for result in self.conn.target.plesk_api().send(plesk_ops.DbServerOperator.Get(plesk_ops.DbServerOperator.FilterAll()))
		}

		subscription_target_services = {}
		for subscription_id, data in subscriptions_by_id.iteritems():
			name = data.gen_info.name
			hosting = data.hosting
			mail = data.mail
			dns_ips = self._get_target_panel_api().get_domain_dns_server_ips(name)

			db_server_group = {}
			for server in subscription_db_servers[subscription_id]:
				if server.server_id in db_servers:
					db_server_group[server.server_type] = 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:
					safe.fail_subscription(subscription.name, u"Subscription does not exist in PPA or was not provisioned. Check PPA task manager for failed tasks. All transfer operations for that subscription will be skipped.")

		return subscription_target_services

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

	@contextmanager
	def set_security_policy(self, nodes):
		changed_nodes = set()
		for node in nodes:
			try:
				signature, policy_on_node = self._get_security_policy(node)
				if policy_on_node != False:
					changed_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)
				)

		try:
			yield
		finally:
			try:
				for node in changed_nodes:
					try:
						signature, policy_on_node = self._get_security_policy(node)
						if policy_on_node != True:
							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_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_path(
			'transfer-accounts/restore-hosting/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.info(u"Restart IIS for IDN domains on %s.", node.description())
			with node.runner() as runner:
				runner.sh(u'iisreset')	

	def _restore_hosting(self, options, subscription_target_services_param, finalize):
		"""TODO subscription_target_services, subscription_target_services_param are not used"""
		logger.info(u"Restore hosting settings")

		self._read_migration_list_lazy(options)
		subscriptions_by_source = self._get_all_subscriptions_by_source()
		subscription_target_services = self._get_subscription_target_services() 
		safe = self._get_safe_lazy()

		with self.set_security_policy(self._get_nodes_to_set_security_policy()):
			subscription_by_name = {subscription.name: subscription for subscription in self._load_ppa_model().iter_all_subscriptions()}

			with self.conn.target.main_node_runner() as runner:
				for plesk_id, path in self._iter_converted_backups_paths():
					with log_context(plesk_id), \
						closing(self.load_converted_plesk_backup(self.source_plesks[plesk_id])) as backup:

						# When subscription is provisioned by CBM, it always has physical hosting turned on.
						# If domains has another type of hosting (not physical) it will be switched during
						# restoration from backup. However, if domains has no hosting then Plesk leaves physical
						# hosting after restoration from backup.
						# Thus it is needed to manually switch off hosting for such subscriptions.
						logger.debug(u"Reset hosting for domains without hosting")
						for subscription_name in subscriptions_by_source[plesk_id]:
							if backup.get_subscription(subscription_name).hosting_type == 'none':
								with safe.try_subscription(subscription_name, u"Failed to reset hosting for subscription without hosting."):
									results = self.conn.target.plesk_api().send(plesk_ops.SubscriptionOperator.Set(
										filter=plesk_ops.SubscriptionOperator.FilterByName([subscription_name]),
										hosting=plesk_ops.SubscriptionHostingNone(),
										limits=None,
									), plesk_disable_provisioning='true')
									for result in results:
										result.check()

						disable_apsmail_provisioning = {
							subscription: self._is_mail_assimilated_subscription(subscription_by_name[subscription], subscription_target_services)
							for subscription in subscriptions_by_source[plesk_id]
						}

						self._call_plesk_restore_hosting(backup, path, runner, subscriptions_by_source[plesk_id], safe=safe, disable_apsmail_provisioning=disable_apsmail_provisioning)

						self._restore_catch_all_smartermail_assimilate(disable_apsmail_provisioning, backup)

		self._fix_iis_idn_401_error()
		self._transfer_ip_access_restrictions()

		self._finalize(finalize, options)

	def _restore_catch_all_smartermail_assimilate(self, disable_apsmail_provisioning, backup):
		"""This is a HACK: when webspace is created we do not pass disable provisioning flag, even if working in SmarterMail assimilate mode.
		Actually it is quite difficult to pass it when creating webspace.
		So, provisioning happens, and SmarterMail APS resets catch-all configuration.
		"""

		logger.debug("Restore catch-all settings for SmarterMail assimilation mode")
		safe = self._get_safe_lazy()
		for subscription_name, is_assimilate in disable_apsmail_provisioning.iteritems():
			with safe.try_subscription(subscription_name, u"Failed to restore mail catch-all settings of domain."):
				if is_assimilate:
					backup_subscription = backup.get_subscription(subscription_name)
					if backup_subscription.mailsystem is not None:
						catch_all_value = backup_subscription.mailsystem.get_catch_all()
						if catch_all_value is not None and '@' in catch_all_value:
							subscription_info = self.conn.target.plesk_api().send(
								plesk_ops.SubscriptionOperator.Get(
									filter=plesk_ops.SubscriptionOperator.FilterByName([subscription_name]), 
									dataset=[plesk_ops.SubscriptionOperator.Dataset.GEN_INFO]
								)
							)
							if len(subscription_info) != 1:
								raise Exception("Expected exactly one subscription, got %s", len(subscription_info))
							subscription_id, _ = subscription_info[0].data

							for nonexistent_user in [
								# first disable catch-all, so when enabling catch-all APSMail won't decide that there is nothing to change
								plesk_ops.MailOperator.SetPrefs.NonexistentUserReject(), 
								plesk_ops.MailOperator.SetPrefs.NonexistentUserForward(catch_all_value), 
							]:
								results = self.conn.target.plesk_api().send(plesk_ops.MailOperator.SetPrefs(
									filter=plesk_ops.MailOperator.FilterBySiteId([subscription_id]),
									nonexistent_user=nonexistent_user
								))
								for result in results:
									result.check()

	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
						)

	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 _call_plesk_restore_hosting(
			self, backup, backup_path, ppa_runner, domains, safe,
			disable_apsmail_provisioning):
		parallels.common.restore_hosting.call_plesk_restore_hosting(
			self.conn.target, backup, backup_path, ppa_runner, domains, safe, 
			disable_apsmail_provisioning=disable_apsmail_provisioning,
			target_backup_path=self.conn.target.main_node_session_file_path('plesk.backup'),
			import_env='PPA_IGNORE_FILE_MATCHING=true',
		)

	def _set_apache_restart_interval(self):
		if self.apache_restart_interval == 'none':
			# none is a special value to skip setting apache restart interval
			return

		if self.conn.target.is_windows:
			# no Apache on target Windows
			return

		# We always require to add mail hosting to the transferred subscriptions.
		# PPA will add a webmail site alias for this subscription, and this will involve apache restart at least
		# on PPA management node. So, apache is involved as soon as we have any subscriptions to transfer.
		ppa_model = self._load_ppa_model()
		apache_involved = len(ppa_model.iter_all_subscriptions()) > 0

		saved_restart_interval = read_yaml(
			self.session_files.get_path_to_apache_restart_interval(), True, None
		)
		if saved_restart_interval is not None:
			logger.warning("The migration tool has detected that the old Apache restart interval was not completely restored, probably because the tool was terminated abnormally. The tool will restore this interval (%s) upon completing the transfer." % saved_restart_interval)

		def get_apache_restart_interval_value():
			with self.conn.target.main_node_runner() as runner:
				return int(runner.sh('/usr/local/psa/bin/server_pref --get-apache-restart-interval'))

		old_interval_value = saved_restart_interval
		if apache_involved:
			logger.info("Set Apache restart interval to %s seconds", self.apache_restart_interval)
			try:
				if old_interval_value is None:
					old_interval_value = get_apache_restart_interval_value()
					write_yaml(
						self.session_files.get_path_to_apache_restart_interval(), 
						old_interval_value
					)
				self._set_apache_restart_interval_value(self.apache_restart_interval)
			except Exception as e:
				logger.debug(u'Exception:', exc_info=e)
				logger.error("Failed to set Apache restart interval value to %s seconds: %s", self.apache_restart_interval, str(e))

	def _restart_apache_nodes_ppa(self):
		for apache_node_id in [1] + self._get_target_panel_api().listServiceNodeIds('ppa_apache'):
			with self.conn.target.ppa_unix_node_runner(apache_node_id) as runner:
				plesk_root = plesk_utils.get_unix_product_root_dir(runner)
				runner.sh("%s/admin/bin/websrvmng -r" % plesk_root)

	def _restart_apache_plesk(self):
		with self.conn.target.main_node_runner() as runner:
			plesk_root = plesk_utils.get_unix_product_root_dir(runner)
			runner.sh("%s/admin/bin/websrvmng -r" % plesk_root)

	def _restore_apache_restart_interval(self):
		if self.apache_restart_interval == 'none':
			# none is a special value to skip setting apache restart interval
			return

		if self.conn.target.is_windows:
			# no Apache on target Windows
			return

		try:
			old_interval_value = read_yaml(
				self.session_files.get_path_to_apache_restart_interval()
			)
			if old_interval_value is not None:
				logger.info("Restore old Apache restart interval value")
				self._set_apache_restart_interval_value(old_interval_value)

				logger.info("Force Apache restart on PPA Management node and Apache nodes")
				if self.target_panel == TargetPanels.PPA:
					self._restart_apache_nodes_ppa()
				else:
					self._restart_apache_plesk()

				os.remove(self.session_files.get_path_to_apache_restart_interval())

		except Exception as e:
			logger.debug(u'Exception:', exc_info=e)
			logger.error("Failed to restore the old Apache restart interval value: %s", str(e))

	def _set_apache_restart_interval_value(self, new_value):
		with self.conn.target.main_node_runner() as runner:
			runner.sh('/usr/local/psa/bin/server_pref -u -restart-apache {new_value}', dict(new_value=new_value))

	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 == TargetPanels.PLESK:
						# 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.info(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.info(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 verify_hosting(self, options, finalize=True):
		"""Verify that all subscriptions and sites are restored.
		"""
		safe = self._get_safe_lazy()

		ppa_model = self._load_ppa_model()
		self._read_migration_list_lazy(options)

		# Get subscription and site names from source Plesks.
		subscription_names = set([s.name for s in ppa_model.iter_all_subscriptions()])
		subscriptions_by_source = self._get_all_subscriptions_by_source()
		site_names = set()
		site_to_subscription = {}
		for plesk_id, _ in self._iter_converted_backups_paths():
			with log_context(plesk_id), \
				closing(self.load_converted_plesk_backup(self.source_plesks[plesk_id])) as backup:
				for subscription_name in subscriptions_by_source[plesk_id]:
					subscription_site_names = backup.get_subscription_site_names(subscription_name)
					site_names.update(subscription_site_names)
					for site_name in subscription_site_names:
						site_to_subscription[site_name] = subscription_name

		logger.debug(u"Objects found in backups: subscriptions=%r, sites=%r", subscription_names, site_names)

		logger.debug(u"Checking which of the subscriptions being transferred exist in PPA")
		existing_subscription_names = set()
		for subscription_names_chunk in partition_list(list(subscription_names), chunk_size=1):
			existing_subscription_names.update([
				result.data[1].gen_info.name 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
			])

		logger.debug(u"Subscriptions found in PPA: %r", existing_subscription_names)

		if not existing_subscription_names >= subscription_names:
			missing_subscriptions = subscription_names - existing_subscription_names
			logger.error(u"Not all subscriptions were restored, missing: %s", ",".join(missing_subscriptions))
			for subscription_name in missing_subscriptions:
				safe.fail_subscription(subscription_name, u"Subscription was not restored to Plesk (unable to find it with Plesk API).")

		logger.debug(u"Checking which of the sites being transferred exist in PPA")
		existing_site_names = set()
		for site_names_chunk in partition_list(list(site_names), chunk_size=1):
			existing_site_names.update([
				result.data[1].gen_info.name 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
			])
		logger.debug(u"Sites found in PPA: %r", existing_site_names)

		if not existing_site_names >= site_names:
			missing_sites = site_names - existing_site_names
			logger.error(u"Not all sites were restored, missing: %s",	",".join(missing_sites))
			for site_name in missing_sites:
				safe.fail_subscription(site_to_subscription[site_name], (u"Site '%s' of subscription was not restored to Plesk (unable to find it with Plesk API)." % site_name))

		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):
		self._check_updates()
		self._check_connections(options)
		self._import_resellers(options)

	def _import_resellers(self, options):
		# 1) fetch Plesk backup if necessary to read resellers from
		self.action_runner.run(
			self.workflow.get_shared_action('fetch-source'), 'fetch-source'
		)
		# 2) read resellers from migration list
		resellers_migration_list = self._read_migration_list_resellers(options)
		# 3) for the resellers from migration list, fetch them from PPA if they exist in PPA
		existing_resellers = self._fetch_resellers_from_ppa(resellers_migration_list or [])
		# 4) 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)
		# 5) 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)
		# 6) print final report and exit
		self._resellers_print_summary(converted_resellers, safe.failed_objects.resellers) 
		self._resellers_exit_failed(safe.failed_objects.resellers)

	def import_plans(self, options):
		if self.target_panel != TargetPanels.PLESK:
			raise MigrationError(strip_multiline_spaces("""
				Import plans command is supported only when migrating to Parallels Plesk.
				For other target panels configure service plans (service templates) manually.
			"""))

		self._check_updates()
		self._check_connections(options)
		self._import_plans(options)

	def _import_plans(self, options):
		# 1) fetch Plesk backup to read reseller and plan information from
		self.action_runner.run(
			self.workflow.get_shared_action('fetch-source'), 'fetch-source'
		)
		# 2) read migration list data
		plan_migration_list = self._read_migration_list_plans(options)
		# 3) convert plans to target model
		converted_plans = PlansConverter().convert_plans(
			self._get_plesk_infos(), self._get_target_panel_api(), plan_migration_list, 
			PlansConverter.convert_plan_settings_from_plesk_backup_to_plesk_import_api
		)
		# 4) import plans to the target panel
		self._get_importer(None).import_plans(converted_plans)

	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._get_plesk_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)
		plain_report = checking.PlainReport(report, *self._extract_source_objects_info())
		converter.convert_plesks(
			self._get_plesk_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 _import_resellers_and_plans(self, options):
		if self.target_panel == TargetPanels.PLESK:
			# Automatically create resellers and plans when migrating to Plesk.
			# It means that customer can run migration in just one simple step
			# - "transfer-accounts" with no need to remember "import-resellers"
			# and "import-plans".  Unfortunately we can't do the same for PPA.
			# It has much more complex model, and we can't automate creating
			# reseller subscriptions and configuring their plans.
			self._import_resellers(options)
			self._import_plans(options)
			# Re-fetch data for further conversions to detect new resellers and
			# their plans
			self.action_runner.run(
				self.workflow.get_shared_action('fetch-source'), 'fetch-source'
			)
			self._fetch(options, options.reload_source_data)

	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)

	# ======================== IP access restrictions ==========================

	def _transfer_ip_access_restrictions(self):
		"""Transfer IP access restriction settings
		"""
		site_infos = []	# [obj(site_name, subscription_name, plesk_id)]
		for plesk_id, subscription_names in self._get_all_windows_subscriptions_by_source().iteritems():
			for subscription_name in subscription_names:
				subscription = self._create_migrated_subscription(subscription_name)
				for site in subscription.converted_backup.iter_domains():
					if site.hosting_type not in ('phosting', 'vrt_hst', 'sub_hst'):
						logger.debug(u"Skip transfer of access restriction settings for site '%s' of subscription '%s' as is has no virtual hosting" % (site.name, subscription.name))
						continue

					site_infos.append(obj(site_name=site.name, subscription_name=subscription_name, plesk_id=plesk_id))

		logger.info(u"Transfer access restriction settings for %d sites in %d subscriptions" % (len(site_infos), len(group_by(site_infos, lambda si: si.subscription_name))))

		safe = self._get_safe_lazy()
		for subscription_name, subscription_site_infos in group_by(site_infos, lambda si: si.subscription_name).items(): # group by subscriptions just for better progress display
			logger.debug(u"Transfer access restriction settings for sites of subscription '%s'" % subscription_name)
			for site_info in subscription_site_infos:
				with safe.try_subscription(site_info.subscription_name, u"Transfer access restriction settings of site '%s'" % site_info.site_name, is_critical=False):
					restrictions = self._get_access_restrictions(site_info.site_name, site_info.plesk_id)
					if restrictions is not None:
						ip_allow, ip_deny = restrictions 
						self._get_target_panel_api().set_ip_access_restrictions(site_info.site_name, ip_allow, ip_deny)

	def _get_access_restrictions(self, site_name, plesk_id):
		"""Get access restrictions (IP-deny, IP-allow),
		   makes sense for Plesk for Windows and for H-Sphere
		"""
		return None	# not implemented for Plesk, as these restrictions were added into UI in the current version (11.5) only
				# TODO transfer them when we'll add migration to PPA 12.0 or like that


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

	@trace('test-all', u"Check that the services and transferred domains are working correctly.")
	def test_all(self, options):
		self._prepare_for_post_migration_checks(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(
			options, report_title="Transferred Domains' Functional Issues",
			checks_source=checks_source,
			report_filename="test_all_report",
			print_hosting_report=print_backup_hosting_report
		)

	@trace('test-services', u"Checking whether service operate properly.")
	def test_services(self, options):
		self._prepare_for_post_migration_checks(options, check_services=False)
		self._run_hosting_checks(
			options, report_title=u"Services' Issues",
			checks_source=ServiceCheckSource(
				self._get_target_panel().get_service_nodes(self.conn.target),
				self.conn.target
			),
			report_filename="test_service_report",
			print_hosting_report=print_service_hosting_report
		)

	@trace('test-sites', u"Checking whether the transferred websites operate properly.")
	def test_sites(self, options):
		self._prepare_for_post_migration_checks(options)
		self._run_hosting_checks(
			options, report_title=u"Transferred Web Sites' Issues",
			checks_source=self._get_web_hosting_checks_data_source(),
			report_filename="test_sites_report",
			print_hosting_report=print_backup_hosting_report
		)

	@trace('test-dns', u"Check that the DNS queries for transferred domains' records are ok.")
	def test_dns(self, options):
		self._prepare_for_post_migration_checks(options)
		self._run_hosting_checks(
			options, report_title=u"Transferred Domains DNS Issues",
			checks_source=self._get_dns_hosting_checks_data_source(options),
			report_filename="test_dns_report",
			print_hosting_report=print_backup_hosting_report
		)

	@trace('test-mail', u"Check whether IMAP, SMTP and POP3 authentication work for mailboxes of the transferred domains.")
	def test_mail(self, options):
		self._prepare_for_post_migration_checks(options)
		self._run_hosting_checks(
			options, report_title=u"Transferred Domains Mail Issues",
			checks_source=self._get_mail_hosting_checks_data_source(),
			report_filename="test_mail_report",
			print_hosting_report=print_backup_hosting_report
		)

	@trace('test-users', u"Check that users are transferred correctly (FTP/SSH/RDP access)")
	def test_users(self, options):
		self._prepare_for_post_migration_checks(options)
		self._run_hosting_checks(
			options, report_title=u"Transferred Users Issues",
			checks_source=self._get_users_hosting_checks_data_source(),
			report_filename="test_users_report",
			print_hosting_report=print_backup_hosting_report
		)

	@trace('test-databases', u"Check whether the databases were copied correctly.")
	def test_databases(self, options):
		self._prepare_for_post_migration_checks(options)
		self._run_hosting_checks(
			options, report_title=u"Transferred Domains Database Issues",
			checks_source=self._get_database_hosting_checks_data_source(),
			report_filename="test_databases_report",
			print_hosting_report=print_backup_hosting_report
		)

	def _prepare_for_post_migration_checks(self, options, check_services=True):
		if check_services:
			self._check_connections(options)
		else:
			self._check_target()
			self._check_sources()
		self.action_runner.run(
			self.workflow.get_shared_action('fetch-source'), 'fetch-source'
		)
		self._fetch(options, options.reload_source_data)
		self._read_migration_list_lazy(options)
		self.convert(
			options,
			# we do not want to see pre-migration and conversion messages when calling post-migration verification
			# because user should already executed 'transfer-accounts', or 'check' commands, which displayed these messages
			merge_pre_migration_report=False 
		)
		self._convert_hosting()

	def _run_hosting_checks(
		self, options, report_title, checks_source, report_filename,
		print_hosting_report
	):
		report = checking.Report(report_title, None)
		util_name = get_executable_file_name()
		check_hosting_checks_source(
			checks_source, report, 
			messages.get_solutions(self.target_panel, util_name),
			self._get_hosting_checkers_config()
		)

		self._finalize_test(report, report_filename, print_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, 
				self._get_target_panel_api()
			)

		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():
				subscription_target_services = self._get_subscription_target_services_cached()
				return group_by(
					self._list_databases_to_copy(subscription_target_services), 
					lambda d: d.subscription_name
				)

			@staticmethod
			def get_source_mysql_client_cli(db):
				return self._get_source_mysql_client_cli(
					db.src_server_access, db.src_db_server
				)

			@staticmethod
			@contextmanager
			def get_source_pgsql_runner(db):
				with ssh_utils.connect(db.src_server_access) as src_ssh:
					yield run_command.SSHRunner(
						src_ssh, 
						"source server '%s' ('%s')" % (
							db.src_server_access.id, 
							db.src_server_access.ip
						)
					)

			@staticmethod
			@cached
			def list_target_panel_databases(subscription_name, database_type):
				return self._get_target_panel_api().list_databases(
					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 _get_source_mysql_client_cli(self, src_server_access, src_db_server):
		"""Return object that executes MySQL client commands on source server.
		
		Overidden in H-Sphere migrator to execute MySQL client directly on
		H-Sphere MySQL node, not on H-Sphere main node

		Arguments:
		- src_server_access - access data (SSH/Windows credentials, IP address
		  and so on) to physical server
		- src_db_server - database server data (host, port, login, password)

		See _list_databases_to_copy function for more details on arguments
		format
		"""

		if not src_server_access.is_windows:
			@contextmanager
			def runner():
				with ssh_utils.connect(src_server_access) as src_ssh:
					yield run_command.SSHRunner(
						src_ssh, 
						"source server '%s' ('%s')" % (src_server_access.id, src_server_access.ip)
					)

			return UnixMySQLClientCLI(
				HostingCheckerRunnerAdapter(runner)
			)
		else:
			@contextmanager
			def runner():
				source_server = self._get_source_node(src_server_access.id)
				with source_server.runner() as runner:
					yield runner

			with runner() as runner_source:
				source_plesk_dir = self._get_path_to_mysql_on_source_server(runner_source)
			return WindowsMySQLClientCLI(
				HostingCheckerRunnerAdapter(runner),
				source_plesk_dir
			)

	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"""

		class MigrationSubscriptionImpl(MigrationSubscription):
			def __init__(self, name):
				self.name = name

			@property
			def name_idn(self):
				return self.name.encode('idna')

			@property
			def full_raw_backup(self_):
				source_settings = self.source_plesks[self_.model.source]
				return self.load_raw_plesk_backup(source_settings)

			@property
			def raw_backup(self_):
				return self_.full_raw_backup.get_subscription(self_.name)

			@property
			def raw_mail_backup(self_):
				mail_server_id = self._get_mail_server_id(self_.model.source)
				source_settings = self._get_source_servers()[mail_server_id]
				backup = self.load_raw_plesk_backup(source_settings)
				try:
					return backup.get_subscription(self_.name)
				except SubscriptionNotFoundException:
					# subscription is not presented on centralized mail server
					# so we consider it has no mail
					return None

			@property
			def full_converted_backup(self_):
				source_settings = self.source_plesks[self_.model.source]
				return self.load_converted_plesk_backup(source_settings)

			@property
			def converted_backup(self_):
				return self_.full_converted_backup.get_subscription(self_.name)

			@property
			def converted_mail_backup(self_):
				mail_server_id = self._get_mail_server_id(self_.model.source)
				source_settings = self._get_source_servers()[mail_server_id]
				backup = self.load_converted_plesk_backup(source_settings)
				try:
					return backup.get_subscription(self_.name)
				except SubscriptionNotFoundException:
					# subscription is not presented on centralized mail server
					# so we consider it has no mail
					return None

			@property
			def model(self_):
				ppa_model = self._load_ppa_model(False)

				return find_only(
					ppa_model.iter_all_subscriptions(), 
					lambda s: s.name == self_.name,
					"Failed to find subscription by name"
				)

			@property
			def target_public_mail_ipv4(self_):
				return self_._get_public_ip(self_.target_mail_ip)

			@property
			def target_public_mail_ipv6(self_):
				return self_._get_public_ip(self_.target_mail_ipv6)

			@property
			def target_public_web_ipv4(self_):
				return self_._get_public_ip(self_.target_web_ip)

			@property
			def target_public_web_ipv6(self_):
				return self_._get_public_ip(self_.target_web_ipv6)

			def _get_public_ip(self_, ip):
				return {
					ip.ip_address: ip.public_ip_address or ip.ip_address
					for ip in self._get_target_panel_api().get_all_ips()
				}.get(ip)

			@property
			def target_mail_ip(self_):
				subscription_target_services = \
					self._get_subscription_target_services_cached()
				return if_not_none(
					if_not_none(
						subscription_target_services.get(self_.name), 
						lambda addresses: addresses.mail_ips
					),
					lambda mail_ips: mail_ips.v4
				)

			@property
			def target_mail_ipv6(self_):
				subscription_target_services = \
					self._get_subscription_target_services_cached()
				return if_not_none(
					if_not_none(
						subscription_target_services.get(self_.name), 
						lambda addresses: addresses.mail_ips
					),
					lambda mail_ips: mail_ips.v6
				)

			@property
			def source_mail_ip(self_):
				mail_server_id = self._get_mail_server_id(self_.model.source)
				source_settings = self._get_source_servers()[mail_server_id]
				return self._get_mailserver_ip_by_subscription_name(
					source_settings, self_.name
				).v4

			@property
			def source_mail_ipv6(self_):
				mail_server_id = self._get_mail_server_id(self_.model.source)
				source_settings = self._get_source_servers()[mail_server_id]
				return self._get_mailserver_ip_by_subscription_name(
					source_settings, self_.name
				).v6

			@property
			def target_web_ipv6(self_):
				subscription_target_services = \
					self._get_subscription_target_services_cached()
				return if_not_none(
					if_not_none(
						subscription_target_services.get(self_.name),
						lambda addresses: addresses.web_ips
					),
					lambda web_ips: web_ips.v6
				)

			@property
			def target_web_ip(self_):
				subscription_target_services = \
					self._get_subscription_target_services_cached()
				return if_not_none(
					if_not_none(
						subscription_target_services.get(self_.name),
						lambda addresses: addresses.web_ips
					),
					lambda web_ips: web_ips.v4
				)

			@property
			def source_web_ip(self_):
				return self._get_subscription_content_ip(
					self_.model
				)

			@property
			def target_dns_ips(self_):
				return self._get_target_panel_api().get_domain_dns_server_ips(self_.name)

			@property
			def source_dns_ips(self_):
				return self._get_source_dns_ips(self_.model.source)

			@property
			def is_windows(self_):
				# implementation is not straightforward because of H-Sphere:
				# we can have both Windows and Unix subscriptions in backup
				return self._subscription_is_windows(
					self_.name, self_.model.source
				)

			@property
			def is_fake(self_):
				return self._is_fake_domain(self_.name)

			@property
			def web_target_server(self_):
				return self._get_subscription_nodes(self_.name).web

			@property
			def web_source_server(self_):
				return self._get_source_web_node(self_.name)

			@property
			def mail_target_server(self_):
				return self._get_subscription_nodes(self_.name).mail

			@property
			def mail_source_server(self_):
				return self._get_source_mail_node(self_.name)

			@property
			def db_target_servers(self_):
				return self._get_subscription_nodes(self_.name).database

			@property
			def dns_target_servers(self_):
				return self._get_subscription_nodes(self_.name).dns

			@property
			def suspended_source(self_):
				source_server = self_.web_source_server
				return self._is_subscription_suspended_on_source(
					source_server, self_.name
				)

			@property
			def suspended_target(self_):
				return plesk_api_utils.is_subscription_suspended(
					self.conn.target.plesk_api(), self_.name
				)

			def add_report_issue(self_, report, problem, solution):
				plain_report = checking.PlainReport(report, *self._extract_source_objects_info())
				plain_report.add_subscription_issue(
					self_.model.source, self_.name, problem, solution
				)

		return MigrationSubscriptionImpl(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,
		]
		if any(len(obj) > 0 for obj in failed_objects_lists):
			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", 
			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:
			if len(objects.keys()) > 0:
				logger.error(
					u"Failed to perform operation on %s %s: %s", 
					len(objects.keys()), name, 
					u', '.join([format_func(obj) for obj in objects])
				)

		if len(safe.failed_objects.subscriptions) > 0:
			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_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_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 _add_report_issues(report, failed_object_infos):
		def format_failed_object_info(info):
			if info.exception is not None:
				return u"%s\nException: %s" % (info.error_message, info.exception)
			else:
				return info.error_message

		for info in failed_object_infos:
			report.add_issue(
				checking.Problem(None, checking.Problem.ERROR, format_failed_object_info(info)), info.solution if info.solution is not None else ''
			)

	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)
					self._add_report_issues(user_report, safe.failed_objects.auxiliary_users[(client.login, user.login)])
			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", reseller_name)

			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", client.login)

				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():
			client_report = root_report.subtarget(u"Client", client_login)
			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_plain(self, ppa_runner):
		subscription_target_services = self._get_subscription_target_services_cached()
		safe = self._get_safe_lazy()

		raw_databases = self._list_databases_to_copy(subscription_target_services)
		grouped_databases = group_by(raw_databases, lambda db_info: (
			db_info.target_node, db_info.src_server_access, db_info.src_server_access.is_windows
		))

		for (target_node, src_server_access, is_windows_source_db_server), databases in grouped_databases.iteritems():
			all_subscriptions_safe_messages = dict((db_info.subscription_name, u"Failed to copy content of database(s)") for db_info in databases)
			is_windows_target_node = target_node.is_windows()
			with safe.try_subscriptions(all_subscriptions_safe_messages, is_critical=False):
				if is_windows_source_db_server:
					for db_info in databases:
						if not is_windows_target_node:
							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.db_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.db_name, db_info.subscription_name)
							def _repeat_copy_windows_content():
								self._copy_single_db_content_windows_to_windows(
									target_node,
									src_server_access,
									db_info.src_db_server,
									db_info.dst_db_server,
									db_info.db_name,
									source_dump_filename=db_info.src_dump_filename,
									target_dump_filename=target_node.get_session_file_path('db_backup.sql')
								)
							safe.try_subscription_with_rerun(_repeat_copy_windows_content, db_info.subscription_name, u"Failed to copy content of database '%s'" % (db_info.db_name), is_critical=False, repeat_error=repeat_error)

				else:
					with ssh_utils.connect(src_server_access) as src_ssh:
						for db_info in databases:
							if is_windows_target_node:
								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.db_name), 
									solution=u"Fix service template subscription is assigned to, so databases are provisioned to Unix servers",
									is_critical=False
								)
							else:
								# XXX we should switch to database target node
								# object instead of replacing passwords in
								# read-only namedtuples like we have for
								# Windows database migration
								db_info = db_info._replace( 
									dst_db_server=db_info.dst_db_server._replace(
										password=target_node.password()
									)
								)
								with target_node.runner() as target_runner:
									repeat_error = u"Unable to copy content of database '%s' on subscription '%s'. Trying to copy content again." % (db_info.db_name, db_info.subscription_name)
									def _repeat_copy_linux_content():
										with log_context(u"{src.dbtype}:{src.host}:{src.port}:{db_name}".format(src=db_info.src_db_server, db_name=db_info.db_name)):
											source_runner = run_command.SSHRunner(src_ssh, "source server '%s' ('%s')" % (src_server_access.id, src_server_access.ip))
											self._copy_single_db_content_linux_to_linux(
												source_runner,
												target_runner,
												db_info.src_server_access.ip,
												db_info.src_db_server,
												db_info.dst_db_server,
												db_info.db_name,
												source_dump_filename=db_info.src_dump_filename,
												target_dump_filename=target_node.get_session_file_path('db_backup.sql')
											)
									safe.try_subscription_with_rerun(_repeat_copy_linux_content, db_info.subscription_name, u"Failed to copy content of database '%s'" % (db_info.db_name), is_critical=False, repeat_error=repeat_error)

	def copy_db_content(self, options, finalize=True):
		self._check_updates()
		self._check_connections(options)
		self.action_runner.run(
			self.workflow.get_shared_action('fetch-source'), 'fetch-source'
		)
		self._fetch(options, options.reload_source_data)
		self._read_migration_list_lazy(options)
		self.convert(
			options,
			# we do not want to see pre-migration and conversion messages when calling copy database content,
			# because user should already executed 'transfer-accounts', or 'check' commands, which displayed these messages
			merge_pre_migration_report=False 
		)
		self._convert_hosting()
		self._copy_db_content(options, finalize)

	def _copy_db_content(self, options, finalize=True):
		with self.conn.target.main_node_runner() as runner:
			self._copy_db_content_plain(runner)
		self._finalize(finalize, options)

	def _list_databases_to_copy(self, subscription_target_services):
		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 plesk_id, subscriptions in servers_subscriptions.iteritems():
			with closing(self.load_raw_plesk_backup(self.source_plesks[plesk_id])) as backup:
				source_server = self.conn.get_source_node(plesk_id)

				for subscription in subscriptions:
					with safe.try_subscription(subscription.name, u"Failed to detect databases of subscription."):
						subscription_db_servers = subscription_target_services[subscription.name].db_servers
						target_db_nodes = self._get_subscription_nodes(subscription.name).database

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

							target_db_node = target_db_nodes.get(db.dbtype)
							if target_db_node 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
								))

							dst_db_server = subscription_db_servers[db.dbtype]

							src_host = self.source_plesks[plesk_id].ip if src_db_server.host == 'localhost' else src_db_server.host
							same_port = any([
								src_db_server.port == target_db_node.port(), # exact match
								db.dbtype == 'mssql' and src_db_server.port in (0, 1433) and target_db_node.port() in (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.

							same_host = target_db_node.ip() in resolve_all(src_host)
							if same_port and same_host:
								logger.debug(
									u"Database '{db.name}' from {db.dbtype}"
									u" server at {db.host}:{db.port} remains on"
									u" the same server, content will not be"
									u" copied".format(db=db))
								continue

							if target_db_node.is_external():
								errormessage = (
										u"Migration to external database"
										u" servers is not supported. Database '{db.name}'"
										u" cannot be migrated to the database"
										u" server {target_host}:{target_port}".format(
												db=db,
												target_host=target_db_node.host(),
												target_port=target_db_node.port()
								))
								safe.fail_subscription(subscription.name, errormessage)
								continue

							src_server_access = self._get_db_src_server_access(plesk_id, db)
							source_server = self._get_source_node(plesk_id)

							databases.append(DatabaseToCopy(
								plesk_id=plesk_id,
								subscription_name=subscription.name,
								target_node=target_db_node,
								src_server_access=src_server_access,
								src_dump_filename=source_server.get_session_file_path('db_backup.sql'),
								src_db_server=src_db_server,
								dst_db_server=dst_db_server,
								db_name=db.name
							))

		return databases

	def _get_db_src_server_access(self, plesk_id, db):
		external_db_servers_by_host = group_by_id(self.external_db_servers.values(), lambda s: s.host)
		if db.host in external_db_servers_by_host:
			return external_db_servers_by_host[db.host]
		else:
			return self.source_plesks[plesk_id]

	def _get_src_db_server(self, db, backup):
		# this method is overridden in H-Sphere's migrator, since it does not know source MSSQL server admin credentials
		source_db_servers = { (dbs.host, dbs.port) : dbs for dbs in backup.iter_db_servers() }
		return source_db_servers[(db.host, db.port)]

	def _get_path_to_mysqldump_on_source_server(self, runner_source):
		source_plesk_dir = windows_utils.detect_plesk_dir(runner_source.sh)
		return windows_path_join(source_plesk_dir, "MySQL\\bin\\mysqldump")

	def _get_path_to_mysql_on_source_server(self, runner_source):
		"""Overridden in Helm3 and Helm4 migrator"""
		source_plesk_dir = windows_utils.detect_plesk_dir(runner_source.sh)
		return windows_path_join(source_plesk_dir, "MySQL\\bin\\mysql")

	def _copy_single_db_content_windows_to_windows(self, target_node, src_server_access, src, dst, db_name, source_dump_filename, target_dump_filename):
		source_server = self._get_source_node(src_server_access.id)
		with \
			target_node.runner() as runner_target, \
			source_server.runner() as runner_source:

			if src.dbtype == 'mysql':
				if not self._is_mysql_client_configured(runner_target):
					raise MigrationError((
						u"mysql client binary was not found on %s, database '%s' of subscription will not be copied. Make sure that: \n"
						u"1) MySQL is installed on service node.\n" 
						u"2) MySQL 'bin' directory is added to PATH environment variable, so MySQL client can be simply started with 'mysql' command.\n"
						u"3) If (1) and (2) are ok, restart 'pem' service with 'net stop pem' and then 'net start pem' commands.\n"
						u"Please check migration tool documentation for more details"
					) % (target_node.description(), db_name))

				logger.info(u"Copy content to {dst.host}:{dst.port}".format(dst=dst))
				logger.debug(u"Dump database on source server")
				path_to_mysqldump = self._get_path_to_mysqldump_on_source_server(runner_source)
				runner_source.sh(
					ur'cmd.exe /C "{path_to_mysqldump} -h {host} -P {port} -u{login} -p{password} {db_name} --result-file={source_dump_filename}"',	dict(
						path_to_mysqldump=path_to_mysqldump, host=src.host, port=src.port, login=src.login, password=src.password, db_name=db_name, source_dump_filename=source_dump_filename
					)
				)

				try:
					logger.debug(u"Copy database dump from source to target server with rsync")
					source_server = self._get_source_node(src_server_access.id)
					with self._windows_rsync(source_server, target_node, source_server.ip()) as rsync:
						rsync.sync(
							source_path='migrator/%s' % (
								source_dump_filename[source_dump_filename.rfind('\\')+1:]
							),
							target_path=windows_utils.convert_path_to_cygwin(
								target_dump_filename
							),
						)
				except run_command.HclRunnerException as e:
					logger.debug(u"Exception: ", exc_info=e)
					raise MigrationError((
						u"Rsync failed to copy a database dump from the source (%s) to the target server (%s): %s\n"
						u"""1. This could happen because of a network connection issue. Retry copying the database content with the help of the "copy-db-content" command.\n"""
						u"2. Check whether rsync is installed on the source server."
					) % (src_server_access.ip, target_node.description(), e.stderr))
				except Exception as e:
					logger.debug(u"Exception: ", exc_info=e)
					raise MigrationError(
						u"Rsync failed to copy a database dump from the source (%s) to the target server (%s): %s\n"
						u"""1. This could happen because of a network connection issue. Retry copying the database content with the help of the "copy-db-content" command.\n"""
						u"2. Check whether rsync is installed on the source server."
					% (src_server_access.ip, target_node.description(), str(e)))

				logger.debug(u"Restore database dump on target server")
				mysql_client = self._get_windows_mysql_client(runner_target)
				runner_target.sh(
					ur'{mysql_client} --no-defaults -h {host} -P {port} -u{login} -p{password} {db_name} -e "source {target_dump_filename}"',
					dict(
						mysql_client=mysql_client,
						host=dst.host,
						port=dst.port,
						login=target_node.login(),
						password=target_node.password(),
						db_name=db_name,
						target_dump_filename=target_dump_filename
					)
				)

				logger.debug(u"Remove database dump files")
				runner_source.sh(ur'cmd.exe /C del {source_dump_filename}"', dict(source_dump_filename=source_dump_filename))
				runner_target.sh(
					ur"cmd.exe /C del {target_dump_filename}",
					dict(target_dump_filename=target_dump_filename)
				)
			elif src.dbtype == 'mssql':
				logger.info(u"Copy content to {dst.host}:{dst.port}".format(dst=dst))
				target_plesk_dir = windows_utils.detect_plesk_dir(runner_target.sh)
				runner_target.sh(
					ur'cmd.exe /C "{dbbackup_path} --copy -copy-if-logins-exist -with-data -src-server={src_host} -server-type={db_type} -src-server-login={src_admin} -src-server-pwd={src_password} -src-database={db_name} -dst-server={dst_host} -dst-server-login={dst_admin} -dst-server-pwd={dst_password} -dst-database={db_name}"',
					dict(
						dbbackup_path=windows_path_join(target_plesk_dir, r'admin\bin\dbbackup.exe'),
						db_type=src.dbtype,
						src_host=windows_utils.get_dbbackup_mssql_host(src.host, src_server_access.ip), src_admin=src.login, src_password=src.password,
						dst_host=dst.host, dst_admin=target_node.login(), dst_password=target_node.password(),
						db_name=db_name
					)
				)
			else:
				logger.error(u"Database has unsupported type and hence will not be copied")

	def _copy_single_db_content_linux_to_linux(self, source_runner, target_runner, src_server_ip, src, dst, db_name, source_dump_filename, target_dump_filename):
		logger.info(u"Copy database '{db_name}' content from '{src_host}:{src_port}' to '{dst_host}:{dst_port}'".format(
			db_name=db_name, 
			src_host=src_server_ip, src_port=src.port,
			dst_host=dst.host, dst_port=dst.port
		))

		migrator_utils.copy_db_content_linux(
			source_runner, target_runner, src_server_ip, 
			migrator_utils.DbServerInfo.from_plesk_backup(src), 
			migrator_utils.DbServerInfo.from_plesk_api(dst), 
			db_name, source_dump_filename, target_dump_filename,
		)

	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 _get_windows_mysql_client(self, runner):
		if self.target_panel == TargetPanels.PLESK:
			plesk_dir = windows_utils.detect_plesk_dir(runner.sh)
			return windows_path_join(plesk_dir, "MySQL\\bin\\mysql")
		else:
			return "mysql"

	def _is_mysql_client_configured(self, runner):
		mysql = self._get_windows_mysql_client(runner)
		return runner.sh_unchecked(u'cmd /c "%s" --version' % mysql)[0] == 0

	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:
			logger.info("Refresh components list of Windows service nodes")

			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

	# ======================== generate migration list ===============================

	@classmethod
	def _get_migration_list_class(cls):
		return MigrationList

	def generate_migration_list(self, options):
		self._check_connections(options)
		if options.migration_list_file is None:
			migration_list_file = self._get_session_file_path("migration-list")
		else:
			migration_list_file = options.migration_list_file

		if os.path.exists(migration_list_file):
			if not options.overwrite:
				raise MigrationError(
					u"Migration list file '%s' already exists. To overwrite it, "
					"use --overwrite option""" % (migration_list_file,)
				)
			else:
				logger.info(
					u"Migration list file '%s' already exists, overwrite it",
					migration_list_file
				)

		self.action_runner.run(
			self.workflow.get_shared_action('fetch-source'), 'fetch-source'
		)

		subscription_names = []
		reseller_logins = []
		for _, plesk_settings in self.source_plesks.iteritems():
			with closing(self.load_raw_plesk_backup(plesk_settings)) as backup:
				subscription_names += [ s.name for s in backup.iter_all_subscriptions() ]
				reseller_logins += [ r.login for _, _, r in backup.iter_all_subscriptions_with_owner_and_reseller() if r is not None ]

		logger.info(u"Checking, if webspaces exist in the target panel")
		target_webspaces = self._get_target_panel_api().list_webspaces_brief(subscription_names)
		logger.info(u"Browsing service templates which could be used for migration")
		target_resellers = self._get_target_panel_api().list_resellers(reseller_logins)
		target_service_templates = { 
			None: [ 
				st.name for st in self._get_target_panel_api().get_service_template_list(poa_api.Identifiers.OID_ADMIN) 
			] 
		}
		target_addon_service_templates = { 
			None: [ 
				st.name 
				for st in self._get_target_panel_api().get_addon_service_template_list(poa_api.Identifiers.OID_ADMIN) 
			] 
		}
		for r in target_resellers:
			target_service_templates[r.contact.username] = [ 
				st.name for st in self._get_target_panel_api().get_service_template_list(r.id) 
			]
			target_addon_service_templates[r.contact.username] = [ 
				st.name for st in self._get_target_panel_api().get_addon_service_template_list(r.id) 
			]

		self._get_migration_list_class().write_initial(
			migration_list_file,
			self._get_source_data(),
			[ webspace.name for webspace in target_webspaces ],
			target_service_templates,
			include_addon_plans=self._get_target_panel_api().has_addon_service_templates(),
			target_addon_plans=target_addon_service_templates,
			target_panel=self.target_panel
		)
		logger.info(u"Migration list file template is saved to '%s'", migration_list_file)


	# ======================== copy web content ===============================

	def copy_content(self, options):
		self._check_updates()
		self._check_connections(options)
		self.action_runner.run(
			self.workflow.get_shared_action('fetch-source'), 'fetch-source'
		)
		self._fetch(options, options.reload_source_data)
		self._read_migration_list_lazy(options)
		self.convert(
			options, 
			# we do not want to see pre-migration and conversion messages when calling copy content,
			# because user should already executed 'transfer-accounts', or 'check' commands, which displayed these messages
			merge_pre_migration_report=False 
		)
		self._convert_hosting()
		self._copy_content(options, finalize=True)
		self._finalize(True, options)

	def _copy_content(self, options, finalize=True):
		self.action_runner.run(
			self.workflow
				.get_entry_point('transfer-accounts')
				.get_path('copy-content/web')
		)

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

	# ======================== copy mail content ==============================

	def copy_mail_content(self, options):
		""" Run prerequisite actions, then copy mail content."""
		self._check_updates()
		self._check_connections(options)
		self.action_runner.run(
			self.workflow.get_shared_action('fetch-source'), 'fetch-source'
		)
		self._fetch(options, options.reload_source_data)
		self._read_migration_list_lazy(options)
		self.convert(
			options,
			# we do not want to see pre-migration and conversion messages when calling copy mail content,
			# because user should already executed 'transfer-accounts', or 'check' commands, which displayed these messages
			merge_pre_migration_report=False 
		)
		self._convert_hosting()
		self._copy_mail_content(options, finalize=True)

	def _copy_mail_content(self, options, finalize=True):
		"""Copy mail content for subscriptions selected in migration list."""
		safe = self._get_safe_lazy()

		for subscription in self._iter_all_subscriptions():
			fail_message = (
				u"Failed to copy mail content of subscription. " 
				u"Most probably that happens because of a network-related issue. Please check "
				u"network connections between source and target servers, then re-copy"
				u"mail content with the help of 'copy-mail-content' command"
			)
			repeat_message = (
				"Failed to copy mail content of subscription '%s'. Retrying..." % (
					subscription.name
				)
			)
			def copy():
				issues = []
				logger.info(u"Copy mail content of subscription '%s'", subscription.name)
				self._copy_mail_content_single_subscription(
					self._get_migrator_server(), subscription, issues
				)
				for issue in issues:
					safe.add_issue_subscription(subscription.name, issue)

			safe.try_subscription_with_rerun(
				copy, subscription.name, fail_message, 
				is_critical=False, repeat_error=repeat_message,
			)

		self._finalize(finalize, options)

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

			with target_node.runner() as runner_target:
				checker.check(
					target_node,
					mysql_bin=self._get_windows_mysql_client(runner_target),
					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)

	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,
			windows_rsync=self._windows_rsync
		)

	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)"""
				subscription_target_services = self._get_subscription_target_services_cached()
				return self._list_databases_to_copy(subscription_target_services)

			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_cut_plesk_backup_by_settings(self, settings):
		return self.session_files.get_path_to_cut_plesk_backup(settings.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_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_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)
