import logging
import sys
import os.path
from collections import deque, defaultdict
from contextlib import contextmanager

from parallels.common.utils import migrator_utils, ssh_utils, plesk_utils
from parallels.common.utils.config_utils import ConfigSection
from parallels.common.registry import Registry
from parallels.target_panel_ppa.connections.target_connections import PPATargetConnections
from parallels.common import checking
from parallels.common.logging_context import log_context, subscription_context
from parallels.common.utils.poa_utils import get_host_id_by_ip
from parallels.common.utils.plesk_utils import get_unix_vhost_dir, get_unix_product_root_dir
from parallels.utils import format_list, format_multiline_list, group_by_id, find_only, find_first, exists
from parallels.common.cli.common_cli import CommandTypes
from parallels.common.utils.migrator_utils import format_report, save_report
from parallels.common import MigrationError
from .move_list import read_move_list, filter_move_list
from .move_services import MoveServices, SubscriptionDictStorageFile
from .session_dir import SessionDir, WindowsRemoteSessionDir
from .ppa_data import PPAData
from .move_state import MoveStateFile
from .old_db_limits import OldDbLimits
from .old_ip_limits import OldIpLimitsFile
from .common import WebServiceType, HostingType
from .operations import MoveContext
from .operations.copy_windows_file_permissions import CopyWindowsFilePermissionsOperation
from .operations.copy_windows_web_content import CopyWindowsWebContentOperation


class Command(object):
	def __init__(self, name, command_type=None, brief_description=None, log_description=None, subcommands=None, run_function=None):
		self.name = name
		self.command_type = command_type
		self.brief_description = brief_description
		self.log_description = log_description
		self.subcommands = subcommands
		self.run_function = run_function


class Migrator(object):
	logger = logging.getLogger(__name__)

	def __init__(self, config):
		self.logger.debug(u"START: %s %s" % (sys.argv[0], " ".join("'%s'" % arg for arg in sys.argv[1:])))
		self.logger.info(u"Initialize migrator")
		global_config_section = ConfigSection(config, 'GLOBAL')
		session_dir_suffix = global_config_section.get('session-dir', 'migration-session')
		session_dir_path = os.path.join(Registry.get_instance().get_base_dir(), 'sessions', session_dir_suffix)
		self.session_dir = SessionDir(session_dir_path)
		self.session_dir.init()
		self.conn = PPATargetConnections(config)
		self.ppa_data = PPAData(self.conn)
		self.move_list = None
		self.subscription_source_web_ips = None

		self.source_web_ips_filename = "subscription-source-web-ips.yaml"

	def copy_web_content(self, move_services, try_subscription):
		self._copy_unix_web_content(move_services, try_subscription)
		self._run_windows_iis_operation(move_services, try_subscription, CopyWindowsWebContentOperation(self._create_move_context()))
		self._run_windows_iis_operation(move_services, try_subscription, CopyWindowsFilePermissionsOperation(self._create_move_context()))

	def _copy_unix_web_content(self, move_services, try_subscription):
		subscriptions = [subscription for subscription in move_services.web if subscription.service_type == WebServiceType.Apache]
		if len(subscriptions) == 0:
			return

		self.logger.info(u"Copy content of Unix subscriptions")
		for subscription in subscriptions:
			with try_subscription(subscription.name):
				self.logger.info(u"Copy content of '%s' subscription", subscription.name)
				self._copy_unix_web_content_single_subscription(
					subscription, 
					self.ppa_data.get_subscription_info(subscription.name)
				)

	def _copy_unix_web_content_single_subscription(self, subscription, subscription_info):
		source_ppa_host_id = get_host_id_by_ip(self.conn.poa_api(), subscription.source_ip)
		target_ppa_host_id = get_host_id_by_ip(self.conn.poa_api(), subscription.target_ip)

		with self.conn.ppa_unix_node_runner(source_ppa_host_id) as runner_source, self.conn.ppa_unix_node_runner(target_ppa_host_id) as runner_target:
			domains = self._get_domains_with_content(subscription_info)
			for domain_name, domain_kind in domains.iteritems():
				self.logger.info(u"Copy content of %s '%s'", domain_kind, domain_name)
				with ssh_utils.public_key_ssh_access_runner(runner_target, runner_source) as key_pathname:
					migrator_utils.copy_vhost_content_unix(
						subscription.source_ip, 'root', runner_source, runner_target, domain_name, key_pathname, 
						# vhost directory may be absent in Plesk 11.5 for addon domain 
						# (www root is in parent domain's dir VHOSTS_D/<parent-domain-name>, system data is in VHOSTS_D/system/<addon-name>, 
						# so there is no data for VHOSTS_D/<addon-domain-name>) 
						# so we use 'skip_if_source_dir_not_exists' flag (but vhost system directory must be presented)
						skip_if_source_dir_not_exists=True 
					) 
					migrator_utils.copy_vhost_system_content_unix(subscription.source_ip, 'root', runner_source, runner_target, domain_name, key_pathname)

	def _create_move_context(self):
		return MoveContext(self.conn, self.ppa_data, self.session_dir, self._get_windows_node_session_dir)

	def _run_windows_iis_operation(self, move_services, try_subscription, operation):
		subscriptions = [subscription for subscription in move_services.web if subscription.service_type == WebServiceType.IIS]
		if len(subscriptions) == 0:
			return

		operation.run(try_subscription, subscriptions)

	@staticmethod
	def _get_windows_node_session_dir(runner):
		session_dir = WindowsRemoteSessionDir("c:\\ppa-move-subscriptions", runner) 
		session_dir.init()
		return session_dir

	def remove_web_content(self, move_services, try_subscription):
		def remove_vhost(runner, domain_name):
			if domain_name != "":
				runner.run("/bin/rm", ["-rf", get_unix_vhost_dir(runner, domain_name)])
			else:
				raise Exception(u"attempt to remove vhost with empty domain name")

		for subscription in move_services.web:
			with try_subscription(subscription.name):
				self.logger.info(u"Remove content of '%s' subscription from source node", subscription.name)
				with subscription_context(subscription.name):
					if subscription.service_type == WebServiceType.Apache:
						subscription_info = self.ppa_data.get_subscription_info(
							subscription.name
						)
						source_ppa_host_id = get_host_id_by_ip(self.conn.poa_api(), subscription.source_ip)
						with self.conn.ppa_unix_node_runner(source_ppa_host_id) as runner_source:
							domains = self._get_domains_with_content(subscription_info)
							for domain_name, domain_kind in domains.iteritems():
								self.logger.info(u"Remove content of %s '%s'", domain_kind, domain_name)
								remove_vhost(runner_source, domain_name)

	def _get_domains_with_content(self, subscription_info):
		"""Returns map of domain names to domain types (domain or subdomain)
		   for all domains of the subscription, including it, that [should] have content
		"""
		domains_with_content = {}

		sites_info = self.ppa_data.get_subscription_sites_info(subscription_info.name)
		for domain_info in [subscription_info] + sites_info:
			# domains with virtual hosting: content and configuration live under separate vhost
			# domains with frame forwarding: content lives under separate vhost, configuration in server-wide directory
			# domains with standard forwarding: no content, configuration in server-wide directory
			# domains without hosting: no content and no configuration
			if domain_info.hosting_type in (HostingType.Virtual, HostingType.FrameForwarding): 
				domains_with_content.update({domain_info.name: 'domain'})
			else:
				self.logger.info(u"Domain '%s' should not have content because of hosting type", domain_info.name)

		subdomain_names = set(self.ppa_data.get_subdomain_names(
			[subscription_info.name] + [site.name for site in sites_info]
		))
		for subdomain_name in subdomain_names:
			if subdomain_name in domains_with_content:
				domains_with_content[subdomain_name] = 'subdomain'

		return domains_with_content
	
	def _get_sites_of_subscription(self, subscription_info, skip_subdomains=False):
		domains = {}

		sites_info = self.ppa_data.get_subscription_sites_info(subscription_info.name)
		for domain_info in [subscription_info] + sites_info:
			domains.update({domain_info.name: 'domain'})

		if not skip_subdomains:
			subdomain_names = self.ppa_data.get_subdomain_names(
				# only sites with virtual hosting can have subdomains
				([subscription_info.name] if subscription_info.hosting_type == HostingType.Virtual else []) + 
				([site.name for site in sites_info if site.hosting_type == HostingType.Virtual])
			)
			for subdomain_name in subdomain_names:
				domains.update({subdomain_name: 'subdomain'})

		return domains

	def execute_command(self, command_name, options):
		"""Execute command tracking move state. 
		Migrator records which operations were performed, which of them were failed.
		We implement revertability and repeatability scenarios with this move state.
		"""
		commands = self.get_commands_tree()
		move_state = self._create_move_state()
		self._assign_functions_to_commands(commands, move_state, options)
		commands_by_names = {command.name: command for command in self.flat_iter_commands_tree(commands)}

		move_list = self._read_move_list_lazy(options)
		all_subscription_names = set(move_list.web.keys() + move_list.mail.keys() + move_list.db_mysql.keys())

		command = commands_by_names[command_name]
		self._execute_command(
			move_state, command, all_subscription_names, 
			# If command is compound (consists of several other commands), then we should track state of execution.
			# In that case all leaf commands (commands with no subcommands) that were already executed, should not be executed again.
			# Otherwise, if command is not compound, customer may want to force its execution and ignore its execution status,
			# for example, to re-execute command that was considered successfull by migrator
			ignore_execution_status=command.subcommands is None
		)
		
		self._print_summary(all_subscription_names, move_state, command)
	
	def _print_summary(self, all_subscription_names, move_state, command):
		failed_count = len([
			subscription for subscription in all_subscription_names 
			if self._was_command_failed(move_state, command, subscription)
		])
		all_count = len(all_subscription_names)

		self.logger.info(u"") # put an empty line before the summary
		self.logger.info(u"******************** Summary ********************")
		self.logger.info(
			u"Operation finished successfully for %s out of %s subscriptions", 
			all_count - failed_count, all_count
		)

	@classmethod
	def _was_command_failed(cls, move_state, command, subscription):
		leaf_commands = [child_command for child_command in cls.flat_iter_commands_tree(command)]
		return any([move_state.was_command_failed(subscription, leaf_command.name) for leaf_command in leaf_commands])

	def _create_move_state(self):
		return MoveStateFile(self.session_dir.get_file_path('move-state.yaml'))

	@staticmethod
	def get_commands_tree():
		return Command(
			name='move', command_type=CommandTypes.MACRO,
			brief_description=u"Move subscription to target node in a single step",
			subcommands=[
				Command(
					name='copy-hosting', command_type=CommandTypes.MACRO,
					brief_description=u"Copy web hosting to target node",
					subcommands=[
						Command(
							name='check', command_type=CommandTypes.MACRO,
							brief_description=u"Check for potential move subscription issues",
							log_description=u"check for potential move subscription issues",
						),
						Command(
							name='increase-web-ip-limits', command_type=CommandTypes.MICRO,
							brief_description=u"Increase IP limits of subscriptions to allocate new dedicated IP addresses",
							log_description=u"increase IP limits of subscriptions to allocate new dedicated IP addresses",
						),
						Command(
							name='allocate-web-ip', command_type=CommandTypes.MICRO,
							brief_description=u"Allocate IP (either shared or dedicated) on target web service node for subscription",
							log_description=u"allocate IP on target web service node for subscription",
						),
						Command(
							name='allocate-mail-ip', command_type=CommandTypes.MICRO,
							brief_description=u"Allocate IP on target mail service node for subscription",
							log_description=u"allocate IP on target mail service node for subscription",
						),
						Command(
							name='allocate-db-ip', command_type=CommandTypes.MICRO,
							brief_description=u"Allocate IP on target database service node for subscription",
							log_description=u"allocate IP on target database service node for subscription",
						),
						Command(
							name='recreate-sysuser', command_type=CommandTypes.MICRO,
							brief_description=u"Recreate system users on target node",
							log_description=u"recreate system users on target node",
						),
						Command(
							name='configure-iis-on-target',
							command_type=CommandTypes.MICRO,
							brief_description=u"Configure IIS on target node for moved subscriptions, create base file structure",
							log_description=u"configure IIS on target node",
						),
						Command(
							name='recreate-cronjobs', command_type=CommandTypes.MICRO,
							brief_description=u"Recreate cron jobs on target node",
							log_description=u"recreate cron jobs on target node",
						),
						Command(
							name='recreate-webusers', command_type=CommandTypes.MICRO,
							brief_description=u"Recreate web users on target node",
							log_description=u"recreate web users on target node",
						),
						Command(
							name='recreate-ftpusers', command_type=CommandTypes.MICRO,
							brief_description=u"Recreate FTP users on target node",
							log_description=u"recreate ftp users on target node",
						),
						Command(
							name='recreate-ssl-certificates', command_type=CommandTypes.MICRO,
							brief_description=u"Recreate SSL certificates on target node",
							log_description=u"recreate SSL certificates on target node",
						),
						Command(
							name='recreate-logrotation', command_type=CommandTypes.MICRO,
							brief_description=u"Recreate logrotation configuration on target node",
							log_description=u"recreate logrotation configuration on target node",
						),
						Command(
							name='recreate-mail', command_type=CommandTypes.MICRO,
							brief_description=u"Recreate mail objects on target node",
							log_description=u"recreate mail objects on target node"
						),
						Command(
							name='copy-web-content', command_type=CommandTypes.MICRO,
							brief_description=u"Copy web content of subscription to target node", 
							log_description=u"copy web content from source to target node",
						),
						Command(
							name='recreate-protected-dirs',
							command_type=CommandTypes.MICRO,
							brief_description=u"Recreate protected directories",
							log_description=u"recreate protected directories",
						),
						Command(
							name='recreate-virtual-dirs',
							command_type=CommandTypes.MICRO,
							brief_description=u"Recreate virtual directories",
							log_description=u"recreate virtual directories",
						),
						Command(
							name='recreate-iis-hosting-settings',
							command_type=CommandTypes.MICRO,
							brief_description=u"Recreate IIS hosting settings",
							log_description=u"recreate IIS hosting settings",
						),
						Command(
							name='copy-mail-content', command_type=CommandTypes.MICRO,
							brief_description=u"Copy mail content of subscription to target node",
							log_description=u"copy mail content from source to target node"
						),
						Command(
							name='copy-mail-autoresponder-attachments', command_type=CommandTypes.MICRO,
							brief_description=u"Copy mail autoresponder attachments of subscription to target node",
							log_description=u"copy mail autoresponder attachments from source to target node"
						),
						Command(
							name='copy-databases', command_type=CommandTypes.MICRO,
							brief_description=u"Copy databases of subscription to target node", 
							log_description=u"copy databases from source to target node",
						),
						Command(
							name='recreate-chrooted-shell', command_type=CommandTypes.MICRO,
							brief_description=u"Recreate chrooted shell for sys users if it was enabled", 
							log_description=u"recreate chrooted shell for sys users",
						),
					]
				),
				Command(
					name='configure-apache-on-target',
					command_type=CommandTypes.MACRO,
					brief_description=u"Configure Apache on target node for moved subscriptions",
					log_description=u"configure Apache on target node",
				),
				Command(
					name='change-subscription-ip', command_type=CommandTypes.MACRO,
					brief_description=u"Change subscription's IP addresses in PPA databases",
					subcommands=[
						Command(
							name='change-subscription-web-ip', command_type=CommandTypes.MICRO,
							brief_description=u"Change subscription's web IP address in PPA databases",
							log_description=u"change subscription's web IP address in PPA databases",
						),
						Command(
							name='change-subscription-mail-ip', command_type=CommandTypes.MICRO,
							brief_description=u"Change subscription's mail IP address in PPA databases",
							log_description=u"change subscription's mail IP address in PPA databases",
						),
						Command(
							name='change-subscription-db-ip', command_type=CommandTypes.MICRO,
							brief_description=u"Change subscription's databases IP address in PPA databases",
							log_description=u"change subscription's databases IP address in PPA databases",
						),
					]
				),
				Command(
					name='move-aps-applications',
					command_type=CommandTypes.MACRO,
					brief_description=u"Move APS applications",
					subcommands=[
						Command(
							name='reassign-aps-db-resources', command_type=CommandTypes.MICRO,
							brief_description=u"Reassign APS database resources", 
							log_description=u"reassign APS database resources",
						),
						Command(
							name='reconfigure-aps-applications', command_type=CommandTypes.MICRO,
							brief_description=u"Reconfigure APS applications on target node", 
							log_description=u"reconfigure APS applications on target node",
						),
					]
				),
				Command(
					name='reconfigure-dns', command_type=CommandTypes.MACRO,
					brief_description=u"Apply new IP address to DNS",
					log_description=u"reconfigure DNS",
				),
				Command(
					name='remove-from-source',
					command_type=CommandTypes.MACRO,
					brief_description=u"Remove hosting data from source node",
					subcommands=[
						Command(
							name='remove-apache-config', command_type=CommandTypes.MICRO,
							brief_description=u"Remove Apache vhosts configuration from source node",
							log_description=u"remove Apache vhosts configuration from source node",
						),
						Command(
							name='move-anonftp', command_type=CommandTypes.MICRO,
							brief_description=u"Move anonymous FTP from source node to target node",
							log_description=u"move anonymous FTP from source node to target node",
						),
						Command(
							name='remove-logrotation', command_type=CommandTypes.MICRO,
							brief_description=u"Remove logrotation configuration from source node",
							log_description=u"remove logrotation configuration from source node",
						),
						Command(
							name='remove-webusers',	command_type=CommandTypes.MICRO,
							brief_description=u"Remove web users from source node",
							log_description=u"remove web users from source node",
						),
						Command(
							name='remove-ftpusers',	command_type=CommandTypes.MICRO,
							brief_description=u"Remove ftp users from source node",
							log_description=u"remove ftp users from source node",
						),
						Command(
							name='remove-ssl-certificates', command_type=CommandTypes.MICRO,
							brief_description=u"Remove SSL certificates from source node",
							log_description=u"remove SSL certificates from source node",
						),
						Command(
							name='remove-web-content', command_type=CommandTypes.MICRO,
							brief_description=u"Remove web content of subscription from source node",
							log_description=u"remove web content from source node",
						),
						Command(
							name='remove-sysusers', command_type=CommandTypes.MICRO,
							brief_description=u"Remove system users and content from source node",
							log_description=u"remove system users and content from source node",
						),
						Command( # must follow remove webusers
							name='remove-iis-hosting', command_type=CommandTypes.MICRO,
							brief_description=u"Remove IIS virtual hosts from source node",
							log_description=u"remove IIS virtual hosts from source node",
						),
						Command(
							name='remove-databases', command_type=CommandTypes.MICRO,
							brief_description=u"Remove databases from source node", 
							log_description=u"remove databases from source node",
						),
						Command(
							name='remove-mail', command_type=CommandTypes.MICRO,
							brief_description=u"Remove mail from source node",
							log_description=u"remove mail from source node",
						),
						Command(
							name='free-old-subscription-web-ips', command_type=CommandTypes.MICRO,
							brief_description=u"Free old subscription web IPs in PPA databases",
							log_description=u"free old subscription web IPs in PPA databases"
						),
						Command(
							name='free-old-subscription-mail-ips', command_type=CommandTypes.MICRO,
							brief_description=u"Free old subscription mail IPs in PPA databases",
							log_description=u"free old subscription mail IPs in PPA databases"
						),
						Command(
							name='restore-web-ip-limits', command_type=CommandTypes.MICRO,
							brief_description=u"Restore old IP limits of subscriptions",
							log_description=u"restore old IP limits of subscriptions"
						)
					]
				),
			]
		)

	@classmethod
	def flat_iter_commands_tree(cls, root_command):
		"""Only leaf commands are returned (commands that have no subcommands)"""
		queue = deque([root_command])
		while len(queue) > 0:
			command = queue.popleft()
			yield command
			if command.subcommands is not None:
				for subcommand in command.subcommands:
					queue.append(subcommand)

	def _assign_functions_to_commands(self, command, move_state, options):
		"""
		For each leaf command in the tree (command that has no subcommands), find and assign corresponding function from Migrator object.
		Interface of the assigned command from Migrator object is:
			def f(move_list, try_subscription):
				# perform action for all subscription from move list
				# use try_subscription context manager function which accepts subscription name to wrap all operations related to specified subscription
				# if some exception is thrown inside "with try_subscription(name):" block and is not catched - subscription with name 'name' is considered failed
		Interface of a command.run_function is the same except for move_list - plain list of subscriptions is passed instead, command.run_function wraps reading of move list
		"""
		for command in self.flat_iter_commands_tree(command):
			# only commands that does not contain any subcommands should be processed 
			# they are is mapped to function of this object by name
			def run_function(subscription_names, try_subscription, command_name=command.name):
				move_list = filter_move_list(self._read_move_list_lazy(options), set(subscription_names))
				move_services = self._create_move_services(move_list)
				function_name=command_name.replace('-', '_')
				getattr(self, function_name)(move_services, try_subscription)

			command.run_function = run_function

	def _create_move_services(self, move_list):
		apache_resource_type_ids = self.ppa_data.get_apache_resource_type_ids()
		iis_resource_type_ids = self.ppa_data.get_iis_resource_type_ids()
		return MoveServices(
			move_list=move_list, 
			web_service_types={webspace: self.ppa_data.get_web_type(webspace, apache_resource_type_ids, iis_resource_type_ids) for webspace in move_list.web.iterkeys()},
			source_web_ips_storage=SubscriptionDictStorageFile(self.session_dir.get_file_path('source-web-ips.yaml')),
			target_web_ips_storage=SubscriptionDictStorageFile(self.session_dir.get_file_path('target-web-ips.yaml')),
			source_mail_ips_storage=SubscriptionDictStorageFile(self.session_dir.get_file_path('source-mail-ips.yaml')),
			target_mail_ips_storage=SubscriptionDictStorageFile(self.session_dir.get_file_path('target-mail-ips.yaml')),
			source_db_mysql_hosts_storage=SubscriptionDictStorageFile(self.session_dir.get_file_path('source-db-mysql-hosts.yaml')),
		)

	@classmethod
	def _execute_command(cls, state, command, all_subscription_names, ignore_execution_status=False, leaf_commands_to_execute_names=None):
		"""
		- Execute specified command 'command' for subscriptions specified in 'all_subscription_names'.
		- Track execution state. Consider we have compound (has subcommands, but no function is assigned) and leaf commands (has no subcommands, but some function is assigned). 
		  Rules are the following:
			- for compound commands simply all subcommands are executed (sequence is considered)
			- if leaf command was already executed, it won't be executed again
			- if leaf command was not executed or was failed during previous migrator execution
				- if some previous leaf command failed - just skip the leaf command
				- if all previous leaf commands were successfull - execute the leaf command
		- Log which commands are executed/skipped, etc
		"""
		if leaf_commands_to_execute_names is None:
			leaf_commands_to_execute_names = set([cmd.name for cmd in cls.flat_iter_commands_tree(command)])

		with log_context(command.name):
			cls.logger.debug(u"Execute command '%s' on the following subscriptions: %s", command.name, format_list(all_subscription_names))
			if command.subcommands is None:
				if command.log_description is not None:
					cls.logger.info(u"")	# put an empty line before the block
					cls.logger.info(u"START: %s", command.log_description)

				cls.logger.debug(u"Ignore execution status = %s", ignore_execution_status)

				subscriptions_to_execute = set()
				subscriptions_skip_previous_failed = set()
				subscriptions_skip_already_executed = set()

				if not ignore_execution_status:
					for subscription_name in all_subscription_names:
						if not state.was_executed(subscription_name, command.name):
							if not state.was_failed(subscription_name, commands_to_check=leaf_commands_to_execute_names - set(command.name)):
								subscriptions_to_execute.add(subscription_name)
							else:
								subscriptions_skip_previous_failed.add(subscription_name)
						else:
							if not state.was_command_failed(subscription_name, command.name):
								subscriptions_skip_already_executed.add(subscription_name)
							else:
								# re-execute failed command
								subscriptions_to_execute.add(subscription_name)

				else:
					subscriptions_to_execute = set(all_subscription_names)

				if len(subscriptions_skip_previous_failed) > 0:
					cls.logger.info(u"Skip (failed at previous steps) for subscriptions: %s", format_list(subscriptions_skip_previous_failed))
				if len(subscriptions_skip_already_executed) > 0:
					cls.logger.info(u"Skip (already executed) for subscriptions: %s", format_list(subscriptions_skip_already_executed))
				if len(subscriptions_to_execute) > 0:
					cls.logger.info(u"Execute for subscriptions: %s", format_list(subscriptions_to_execute))
					cls._execute_command_for_subscriptions(state, command, subscriptions_to_execute)

				if command.log_description is not None:
					cls.logger.info(u"FINISH: %s", command.log_description)

				# exit if all subscriptions failed and there is no sence to proceed to the next steps
				if ( # all subscriptions failed ~ all subscriptions could be divided into 2 categories
					len(all_subscription_names) == 
						len([subscription for subscription in all_subscription_names if state.was_command_failed(subscription, command.name)]) + # the 1st category: failed at the current step\
						len(subscriptions_skip_previous_failed) # the 2nd category: failed at the previous steps
				):
					return False
			else:
				for subcommand in command.subcommands:
					could_continue = cls._execute_command(state, subcommand, all_subscription_names, ignore_execution_status, leaf_commands_to_execute_names)
					if not could_continue:
						cls.logger.error(u"") # put an empty line after the block
						cls.logger.error(u"All subscriptions are failed to migrate, stopping.")
						return False

		return True

	@classmethod
	def _execute_command_for_subscriptions(cls, move_state, command, subscription_names):
		"""
		- Execute command for specified subscriptions. But do not considering their current execution state - execute command just always.
		- Save command execution state for each subscription (mark each subscription either as successfully executed, or as failed)
		"""
		failed_this_run = set()

		@contextmanager
		def try_subscription(name):
			try:
				with log_context(name):
					yield
			except Exception as e:
				move_state.set_fail(name, command.name)
				failed_this_run.add(name)
				cls.logger.error(u"Failed to perform an action on subscription '%s'. Exception message: %s", name, e)
				cls.logger.debug(u"Exception:", exc_info=True)

		try:
			command.run_function(subscription_names, try_subscription)
		except Exception as e:
			# overall command failure - mark failed all subscriptions
			cls.logger.error(u"Failed to perform an action on the following subscription(s): %s. Exception message: %s", format_list(subscription_names), e)
			cls.logger.debug(u"Exception:", exc_info=True)
			for subscription_name in subscription_names:
				move_state.set_fail(subscription_name, command.name)
		else:
			for subscription_name in subscription_names:
				if not subscription_name in failed_this_run:
					move_state.set_success(subscription_name, command.name)

	def check(self, move_services, try_subscription):
		report = checking.Report(u"Detected potential issues", None)
		db_server_id_by_name = self.ppa_data.get_db_server_id_by_host()
		self.logger.info(u"Perform pre-migration checks")
		for subscription in move_services.db_mysql:
			with try_subscription(subscription.name):
				subreport = report.subtarget(u"Subscription", subscription.name)
				self.logger.debug(u"Check that database already exists on target server")
				target_db_server_id = db_server_id_by_name[subscription.target_host]
				databases = self.ppa_data.get_mysql_databases(subscription.name)
				
				existing_dbs = {}
				source_dbs = []
				for db in databases:
					if db.db_server_id == target_db_server_id:
						existing_dbs[db.name] = db
					elif db.db_server_id != target_db_server_id:
						source_dbs.append(db)

				for db in source_dbs:
					if db.name in existing_dbs:
						self.logger.debug(u"Database '%s' already exists on host '%s', no need to create", db.name, subscription.target_host)
						subreport.add_issue(
							checking.Problem('mysql-migration-check', checking.Problem.WARNING, u"Database '%s' already exists on host '%s', no need to create" % (db.name, subscription.target_host)),
							u"That might be a aftermath of moving one subscription several times. Content of target database will be migrated."
						)
						self.logger.debug(u"Check that database users already exists on target server")
						existing_db_logins = {existing_db_user.login for existing_db_users in self.ppa_data.get_db_users_for_databases([existing_dbs[db.name].id]).itervalues() for existing_db_user in existing_db_users}
						for source_db_users in self.ppa_data.get_db_users_for_databases([db.id]).itervalues():
							for source_db_user in source_db_users:
								if source_db_user.login in existing_db_logins:
									subreport.add_issue(
										checking.Problem(
											'mysql-migration-check', 
											checking.Problem.WARNING, 
											u"Database user '%s' for '%s' database already exists on host '%s', no need to create" % (source_db_user.login, db.name, subscription.target_host)
										),
										u"That might be a aftermath of moving one subscription several times. Creation of database users will be skipped."
									)

		for subscription in move_services.web:
			with try_subscription(subscription.name):
				subscription_info = self.ppa_data.get_subscription_info(subscription.name)
				if subscription_info.hosting_type == HostingType.NoHosting:
					subreport = report.subtarget(u"Subscription", subscription.name)
					subreport.add_issue(
						checking.Problem(
							'no-hosting-subscription', 
							checking.Problem.ERROR, 
							u"Subscription has no web hosting. Web service of such subscriptions can not be moved."
						),
						u"To migrate other subscriptions and/or services remove '<web>' node of subscription from the move list and restart moving tools."
					)
		
		report_tree = format_report(report, [])
		print report_tree
		path = save_report(report_tree, self.session_dir.get_file_path("pre_move_subscription_report"))
		self.logger.info("The report was saved into the file '%s'" % path)
		if report.has_errors():
			raise MigrationError(
				u"Unable to continue subscription moving until there are no issues at 'error' level in pre-migration checks. " +
				u"Please review pre-migration tree above and fix the errors."
			)
	
	def recreate_sysuser(self, move_services, try_subscription):
		self._run_plesk_move_subscription_command_for_subscriptions_web_target('recreate-sysuser', move_services, try_subscription)

	def recreate_cronjobs(self, move_services, try_subscription):
		self._run_plesk_move_subscription_command_for_subscriptions_web_target('recreate-cronjobs', move_services, try_subscription)

	def recreate_webusers(self, move_services, try_subscription):
		self._run_plesk_move_subscription_command_for_subscriptions_web_target('recreate-webusers', move_services, try_subscription)

	def recreate_ftpusers(self, move_services, try_subscription):
		for subscription in move_services.web:
			with try_subscription(subscription.name):
				self._run_plesk_move_subscription_command('recreate-ftpusers', {'subscription-name': subscription.name, 'target-subscription-ip': subscription.target_ip, 'service-node-ip': subscription.target_service_node_ip})

	def recreate_ssl_certificates(self, move_services, try_subscription):
		for subscription in move_services.web:
			with try_subscription(subscription.name):
				self._run_plesk_move_subscription_command('bind-ssl-certificates', {'subscription-name': subscription.name, 'new-ip': subscription.target_ip})

	def recreate_protected_dirs(self, move_services, try_subscription):
		self._run_plesk_move_subscription_command_for_subscriptions_web_iis_target('recreate-iis-protected-dirs', move_services, try_subscription)

	def recreate_virtual_dirs(self, move_services, try_subscription):
		self._run_plesk_move_subscription_command_for_subscriptions_web_iis_target('recreate-iis-virtual-dirs', move_services, try_subscription)

	def recreate_iis_hosting_settings(self, move_services, try_subscription):
		self._run_plesk_move_subscription_command_for_subscriptions_web_iis_target('recreate-iis-hosting-settings', move_services, try_subscription)

	def increase_web_ip_limits(self, move_services, try_subscription):
		old_ip_limits = self._create_old_ip_limits()
		for subscription in move_services.web:
			with try_subscription(subscription.name):
				limit = SubscriptionIPv4Limit(self._get_poa_api(), subscription.name)
				current_ipv4_limit = limit.fetch()
				old_ipv4_limit = old_ip_limits.get_ipv4_limit(subscription.name)

				if old_ipv4_limit is None:
					old_ip_limits.set_ipv4_limit(subscription.name, current_ipv4_limit)
					old_ipv4_limit = current_ipv4_limit

				webspace_id = self._get_poa_api().get_webspace_id_by_primary_domain(subscription.name)
				is_dedicated_ipv4 = exists(self._get_poa_api().get_webspace(webspace_id).get('ip_type'), lambda x: x['name'] == 'ipv4_type' and x['value'] == 'dedicated')

				if is_dedicated_ipv4: 
					if current_ipv4_limit <= old_ip_limits.get_ipv4_limit(subscription.name): 
						# limit was not touched - increase it by 1 to have guarantee that we can allocate new dedicated IP address on the target service node
						limit.set(old_ipv4_limit + 1)

	def allocate_web_ip(self, move_services, try_subscription):
		self._allocate_ip(move_services, try_subscription, allocate_type='web')
	
	def allocate_mail_ip(self, move_services, try_subscription):
		self._allocate_ip(move_services, try_subscription, allocate_type='mail')

	def _allocate_ip(self, move_services, try_subscription, allocate_type = 'web'):
		for subscription in getattr(move_services, allocate_type):
			with try_subscription(subscription.name):
				if subscription.target_ip is None:
					if allocate_type == 'web':
						if subscription.service_type == WebServiceType.Apache:
							subscription.target_ip = self.conn.poa_api().pleskweb_allocate_ips_for_webspace(
								self._get_webspace_poa_id(subscription.name),
								self._get_host_id(subscription.target_service_node_ip)
							)['ips']['main_ip']
						elif subscription.service_type == WebServiceType.IIS:
							subscription.target_ip = self.conn.poa_api().pleskwebiis_allocate_ips_for_webspace(
								self._get_webspace_poa_id(subscription.name),
								self._get_host_id(subscription.target_service_node_ip)
							)['ips']['main_ip']
					elif allocate_type == 'mail':
						subscription.target_ip = self.conn.poa_api().pleskmail_allocate_ips_for_webspace(
							self._get_webspace_poa_id(subscription.name),
							self._get_host_id(subscription.target_service_node_ip)
						)['ips']['main_ip']
					else:
						assert(False)
				else:
					self.logger.info(u"IP address '%s' was already allocated for subscription '%s'", subscription.target_ip, subscription.name)

				if subscription.source_ip is None:
					subscription_info = self.ppa_data.get_subscription_info(
						subscription.name
					)
					if allocate_type == 'web':
						subscription.source_ip = subscription_info.web_ips.v4 or subscription_info.web_ips.v6
					elif allocate_type == 'mail':
						subscription.source_ip = subscription_info.mail_ips.v4 or subscription_info.mail_ips.v6
					else:
						assert(False)

	def allocate_db_ip(self, move_services, try_subscription):
		db_hosts_by_id = self.ppa_data.get_db_host_by_server_id()

		for subscription in move_services.db_mysql:
			with try_subscription(subscription.name):
				self.conn.poa_api().pleskdb_allocate_mysql_ips_for_webspace(
					self._get_webspace_poa_id(subscription.name),
					self._get_host_id(self._get_db_host_ip(subscription.target_host))
				)

				source_db_hosts = set()
				databases = self.ppa_data.get_mysql_databases(subscription.name)

				# consider source_hosts = default database server for that subscription + all hosts where subscription's databases are located
				# please note that it can include target server
				for database in databases:
					source_db_hosts.add(db_hosts_by_id[database.db_server_id])

				default_mysql_server_id = self.ppa_data.get_default_mysql_server_id(subscription.name)
				if default_mysql_server_id is not None:
					source_db_hosts.add(db_hosts_by_id[default_mysql_server_id])

				subscription.source_hosts = list(source_db_hosts)

	def _get_webspace_poa_id(self, subscription_name):
		return self.conn.poa_api().get_webspace_id_by_primary_domain(subscription_name)

	def _get_host_id(self, node_ip):
		return get_host_id_by_ip(self.conn.poa_api(), node_ip)

	def recreate_logrotation(self, move_services, try_subscription):
		self._run_plesk_move_subscription_command_for_subscriptions_web_target('recreate-logrotation', move_services, try_subscription)

	def recreate_mail(self, move_services, try_subscription):
		self._run_plesk_move_subscription_command_for_subscriptions_mail_target('recreate-mail', move_services, try_subscription)

	def copy_databases(self, move_services, try_subscription):
		db_server_id_by_name = self.ppa_data.get_db_server_id_by_host()
		old_db_limits = self._create_old_db_limits()
		for subscription in move_services.db_mysql:
			with try_subscription(subscription.name):
				target_db_server_id = db_server_id_by_name[subscription.target_host]
				old_limits = old_db_limits.get_limits(subscription.name)
				if old_limits is None:
					old_limits = self.ppa_data.get_database_limits(subscription.name)
					old_db_limits.set_limits(subscription.name, old_limits)

				self.ppa_data.set_subscription_limits(
					subscription.name, 
					{
						limit_name: -1  # set every database limit to unlimited
						for limit_name in old_limits.iterkeys() 
					}
				)

				webspace_id = self.ppa_data.get_webspace_id_by_name(subscription.name)

				databases = self.ppa_data.get_mysql_databases(subscription.name)
				self._check_multiple_databases(databases, target_db_server_id)

				source_databases = [
					db for db in databases 
					if db.db_server_id != target_db_server_id
				]
				existing_databases = {
					db.name for db in databases 
					if db.db_server_id == target_db_server_id
				}

				for db in source_databases:
					if db.name not in existing_databases:
						self.logger.info(u"Create database '%s' on host '%s'", db.name, subscription.target_host)
						self.ppa_data.create_mysql_database(webspace_id, db_server_id_by_name[subscription.target_host], db.name)
					else:
						self.logger.info(u"Database '%s' already exists on host '%s', no need to create", db.name, subscription.target_host)

				self.logger.debug(u"Clone database users")
				self._run_plesk_move_subscription_command('clone-mysql-users', {'subscription-name': subscription.name, 'db-host': subscription.target_host})

				self.logger.debug(u"Set database administrator flag for database users")
				databases = self.ppa_data.get_mysql_databases(subscription.name) # re-fetch databases list to get cloned databases from the target server
				target_db_id_by_name = {db.name: db.id for db in databases if db.db_server_id == target_db_server_id}

				for db in source_databases:
					if db.name in target_db_id_by_name:
						target_db_id = target_db_id_by_name[db.name]

						db_users = self.ppa_data.get_db_users_for_databases([db.id, target_db_id])
						
						source_db_admin_id = self.ppa_data.get_db_admin_user_id(db.id)
						if source_db_admin_id is not None:
							source_db_admin_login = find_only(
								db_users[db.id], lambda user: user.id == source_db_admin_id, 
								u"Unable to find source database administrator login by id"
							).login
							target_db_admin_id = find_only(
								db_users[target_db_id], lambda user: user.login == source_db_admin_login,
								u"Unable to find target database user by login"
							).id
							self.ppa_data.set_db_admin_user_id(target_db_id, target_db_admin_id)
						else:
							pass # there is no database administrator for this database, so there is nothing to change on the target

		self.copy_databases_content(move_services, try_subscription)

	def copy_databases_content(self, move_services, try_subscription):
		db_servers = self.ppa_data.get_db_servers_with_passwords()
		db_server_by_host = group_by_id(db_servers, lambda d: d.host)
		db_server_by_id = group_by_id(db_servers, lambda d: d.id)

		for subscription in move_services.db_mysql:
			target_db_server_id = db_server_by_host[subscription.target_host].id

			with try_subscription(subscription.name):
				databases = self.ppa_data.get_mysql_databases(subscription.name)
				self._check_multiple_databases(databases, target_db_server_id)

				source_databases = [
					db for db in databases 
					if db.db_server_id != target_db_server_id
				]
				target_database_names = {
					db.name for db in databases 
					if db.db_server_id == target_db_server_id
				}

				target_server_info = db_server_by_id[target_db_server_id]
				for db in source_databases:
					source_server_info = db_server_by_id[db.db_server_id]
					if db.name in target_database_names:
						self.logger.info(u"Copy content of database '%s' to server '%s'", db.name, subscription.target_host)
						@contextmanager
						def create_runner(host):
							if host != 'localhost':
								with self.conn.ppa_unix_node_runner(get_host_id_by_ip(self.conn.poa_api(), host)) as runner:
									yield runner
							else:
								with self.conn.main_node_runner() as runner:
									yield runner

						with create_runner(source_server_info.host) as runner_source, create_runner(target_server_info.host) as runner_target:
							self._copy_db_content_linux(
								runner_source=runner_source,
								runner_target=runner_target,
								src_server_ip=source_server_info.host if source_server_info.host != 'localhost' else self.conn.main_node_ip,
								src=migrator_utils.DbServerInfo.from_plesk_api(db_server_by_id[db.db_server_id]),
								dst=migrator_utils.DbServerInfo.from_plesk_api(db_server_by_id[target_db_server_id]),
								db_name=db.name
							)
					else:
						raise Exception(u"Skip copy content of database '%s' to server '%s': database does not exist on target server" % (db.name, subscription.target_host,))

	def _copy_db_content_linux(self, runner_source, runner_target, src_server_ip, src, dst, db_name):
		"""
		Arguments
		- src - instance of DbServerInfo class describing the source database server
		- dst - instance of DbServerInfo class describing the target database server
		"""

		source_dump_filename = '/tmp/db_backup.sql'
		target_dump_filename = '/tmp/db_backup.sql'

		if src.dbtype == 'mysql':
			backup_command = u"mysqldump -h {src_host} -P {src_port} -u{src_admin} --quick --quote-names " \
				u"--add-drop-table --default-character-set=utf8 --set-charset {db_name}"
			# workaround for Plesk feature - it does not tell default MySQL server's admin password
			if src.host == 'localhost' and src.port == 3306:
				backup_command += u" -p\"`cat /etc/psa/.psa.shadow`\""
			else:
				backup_command += u" -p{src_password}"

			restore_command = u"mysql --no-defaults -h {dst_host} -P {dst_port} -u{dst_admin} {db_name}"
			# workaround for Plesk feature - it does not tell default MySQL server's admin password
			if dst.host == 'localhost' and dst.port == 3306:
				restore_command += u" -p\"`cat /etc/psa/.psa.shadow`\""
			else:
				restore_command += u" -p{dst_password}"
		elif src.dbtype == 'postgresql':
			backup_command = u"PGUSER={src_admin} PGPASSWORD={src_password} PGDATABASE={db_name} pg_dump -Fc -b -O -i"
			if src.host != 'localhost':
				backup_command += " -h {src_host} -p {src_port}"
			restore_command = u"PGUSER={dst_admin} PGPASSWORD={dst_password} pg_restore -v -d {db_name} -c"
			if dst.host != 'localhost':
				restore_command += " -h {dst_host} -p {dst_port}"
			if runner_source.run_unchecked('which', ['pg_dump'])[0] != 0:
				raise MigrationError(
					"pg_dump utility is not installed on '%s' server. "
					"Please make sure that:\n"
					"1) All external PostgreSQL servers (other than Plesk servers) are specified in config.ini "
					"in 'external-postgresql-servers' option, and pg_dump utility is installed on them.\n"
					"OR\n"
					"2) pg_dump is installed on all Plesk servers that have domains with PostgreSQL databases.\n"
					"Once the problem is fixed, run 'copy-db-content' migration tool command"
					"to copy only the databases" % src_server_ip
				)
		else:
			raise Exception(u"Database has unsupported type '%s' and hence will not be copied" % src.db_type)

		runner_source.sh(backup_command + u" > {source_dump_filename}", dict(  # backup database on source node
			src_host=src.host,
			src_port=src.port,
			src_admin=src.login,
			src_password=src.password,
			db_name=db_name,
			source_dump_filename=source_dump_filename
		))

		def copy_dump_file(key_pathname):
			runner_target.sh(
				u"scp -i {key_pathname} -o StrictHostKeyChecking=no -o GSSAPIAuthentication=no "
				u"{src_server_ip}:{source_dump_filename} {target_dump_filename}", dict(
					key_pathname=key_pathname,
					src_server_ip=src_server_ip,
					source_dump_filename=source_dump_filename,
					target_dump_filename=target_dump_filename
				)
			)
		with ssh_utils.public_key_ssh_access_runner(runner_target, runner_source) as key_pathname:
			copy_dump_file(key_pathname)

		runner_source.remove_file(source_dump_filename)  # remove backup from source node
		runner_target.sh(restore_command + u" < {target_dump_filename}", dict(  # restore backup on target node
			dst_host=dst.host, dst_port=dst.port,
			dst_admin=dst.login, dst_password=dst.password,
			db_name=db_name,
			target_dump_filename=target_dump_filename,
		))
		runner_target.remove_file(target_dump_filename)  # remove backup from target node


	def _get_db_host_ip(self, db_host):
		if db_host == 'localhost':
			return self.conn.main_node_ip # "localhost" means PPA management node
		else:
			return db_host # otherwise it should be IP
	
	def recreate_chrooted_shell(self, move_services, try_subscription):
		self._run_plesk_move_subscription_command_for_subscriptions_web_target('recreate-chrooted-shell', move_services, try_subscription)

	def reassign_aps_db_resources(self, move_services, try_subscription):
		db_server_id_by_name = self.ppa_data.get_db_server_id_by_host()
		for subscription in move_services.db_mysql:
			with try_subscription(subscription.name):
				self._check_multiple_databases(self.ppa_data.get_mysql_databases(subscription.name), db_server_id_by_name[subscription.target_host])
				self._run_plesk_move_subscription_command('reassign-aps-db-resources', {'subscription-name': subscription.name, 'db-host': subscription.target_host})

	def remove_databases(self, move_services, try_subscription):
		db_server_id_by_name = self.ppa_data.get_db_server_id_by_host()
		old_db_limits = self._create_old_db_limits()

		for subscription in move_services.db_mysql:
			with try_subscription(subscription.name):
				target_db_server_id = db_server_id_by_name[subscription.target_host]
				databases = self.ppa_data.get_mysql_databases(subscription.name)

				self._check_multiple_databases(databases, target_db_server_id)

				source_db_names = [
					db.name for db in databases 
					if db.db_server_id != target_db_server_id
				]
				source_db_ids = [
					db.id for db in databases 
					if db.db_server_id != target_db_server_id
				]
				target_db_names = [
					db.name for db in databases 
					if db.db_server_id == target_db_server_id
				]

				if not (set(source_db_names) <= set(target_db_names)):
					raise Exception(
						u"Not all databases were copied to the target node. No databases of the subscription will be remove from source servers. Here are absent database(s): %s" 
						% (format_list(set(source_db_names) - set(target_db_names)),)
					)
				self.ppa_data.delete_databases(source_db_ids)

				old_limits = old_db_limits.get_limits(subscription.name)
				if old_limits is not None:
					self.ppa_data.set_subscription_limits(subscription.name, old_limits)

	def remove_mail(self, move_services, try_subscription):
		self._run_plesk_move_subscription_command_for_subscriptions_mail_source('remove-mail', move_services, try_subscription)

	@staticmethod
	def _check_multiple_databases(databases, target_db_server_id):
		databases_by_names = defaultdict(list)
		for db in databases:
			if db.db_server_id != target_db_server_id:
				databases_by_names[db.name].append(db)
		multiple_dbs_names = [name for name, dbs in databases_by_names.iteritems() if len(dbs) > 1]
		if len(multiple_dbs_names) > 0:
			raise Exception(
				u"There are some databases with the same name that exist on different hosts: %s. Please rename so there are no such databases and run command for the subscription again." 
				% (format_list(multiple_dbs_names),)
			)

	def change_subscription_web_ip(self, move_services, try_subscription):
		for subscription in move_services.web:
			with try_subscription(subscription.name):
				self.logger.debug(u"Change subscription IP in Plesk")
				self._run_plesk_move_subscription_command('change-subscription-web-ip', {'subscription-name': subscription.name, 'service-node-ip': subscription.target_ip})

				self.logger.debug(u"Change subscription IP in POA")
				if subscription.service_type == WebServiceType.Apache:
					self.conn.poa_api().pleskweb_change_webspace_ips(
						self._get_webspace_poa_id(subscription.name),
						subscription.target_ip
					)
				elif subscription.service_type == WebServiceType.IIS:
					self.conn.poa_api().pleskwebiis_change_webspace_ips(
						self._get_webspace_poa_id(subscription.name),
						subscription.target_ip
					)

	def change_subscription_db_ip(self, move_services, try_subscription):
		for subscription in move_services.db_mysql:
			with try_subscription(subscription.name):
				self.logger.debug(u"Change subscription IP in POA")
				self.conn.poa_api().pleskdb_change_webspace_mysql_ips(
					self._get_webspace_poa_id(subscription.name), 
					self._get_db_host_ip(subscription.target_host)
				)
				self.logger.debug(u"Change subscription's default MySQL server in Plesk")
				self._run_plesk_move_subscription_command('change-mysql-default-server', {'subscription-name': subscription.name, 'db-host': subscription.target_host})

	def change_subscription_mail_ip(self, move_services, try_subscription):
		for subscription in move_services.mail:
			with try_subscription(subscription.name):
				self.logger.debug(u"Change subscription IP in Plesk")
				self._run_plesk_move_subscription_command('change-subscription-mail-ip', {
					'subscription-name': subscription.name, 
					'target-service-node-ip': subscription.target_ip, 
					'source-service-node-ip': subscription.source_ip
				})

				self.logger.debug(u"Change subscription IP in POA")
				self.conn.poa_api().pleskmail_change_webspace_ips(
					self._get_webspace_poa_id(subscription.name),
					subscription.target_ip
				)

	def free_old_subscription_web_ips(self, move_services, try_subscription):
		for subscription in move_services.web:
			with try_subscription(subscription.name):
				if subscription.service_type == WebServiceType.Apache:
					self._get_poa_api().pleskweb_deallocate_ips_for_webspace(
						self._get_webspace_poa_id(subscription.name), 
						subscription.source_ip
					)
				elif subscription.service_type == WebServiceType.IIS:
					self._get_poa_api().pleskwebiis_deallocate_ips_for_webspace(
						self._get_webspace_poa_id(subscription.name), 
						subscription.source_ip
					)

	def free_old_subscription_mail_ips(self, move_services, try_subscription):
		for subscription in move_services.mail:
			with try_subscription(subscription.name):
				self._get_poa_api().pleskmail_deallocate_ips_for_webspace(
					self._get_webspace_poa_id(subscription.name), 
					subscription.source_ip
				)

	# TODO Uncommend the code below when bug #129473 is fixed
	# If you deallocate IPs before the bug is fixed, it may lead to inoperability of management node in certain (rare, but still possible) cases
	#def free_old_subscription_db_ips(self, move_services, try_subscription):
		#for subscription in move_services.db_mysql:
			#with try_subscription(subscription.name):
				#for host in subscription.source_hosts:
					#if host != subscription.target_host:
						#self._get_poa_api().pleskdb_deallocate_mysql_ips_for_webspace(
							#self._get_webspace_poa_id(subscription.name), 
							#self._get_db_host_ip(host)
						#)

	def restore_web_ip_limits(self, move_services, try_subscription):
		old_ip_limits = self._create_old_ip_limits()
		for subscription in move_services.web:
			with try_subscription(subscription.name):
				limit = SubscriptionIPv4Limit(self._get_poa_api(), subscription.name)
				old_ipv4_limit = old_ip_limits.get_ipv4_limit(subscription.name)
				if old_ipv4_limit is not None:
					limit.set(old_ipv4_limit)

	def _get_poa_api(self):
		return self.conn.poa_api()

	def reconfigure_dns(self, move_services, try_subscription):
		for subscription in set(move_services.web + move_services.mail):
			with try_subscription(subscription.name):
				self._run_plesk_move_subscription_command('sync-dns', {'subscription-name': subscription.name})
				self._fix_subdomain_records_in_domain_alias_zone(
					subscription.name, 
					self.ppa_data.get_subscription_info(subscription.name), 
					subscription.source_ip, 
					subscription.target_ip
				)

	def _fix_subdomain_records_in_domain_alias_zone(self, subscription_name, subscription_info, source_ip, target_ip):
		"""In certain cases Plesk does not change DNS record of subdomain in domain alias DNS zone
		Plesk bug #127522, this function is a workaround"""

		sites_info = self.ppa_data.get_subscription_sites_info(subscription_name)
		for domain_info in [subscription_info] + sites_info:
			subdomain_names = self.ppa_data.get_subdomain_names([domain_info.name])
			
			for alias in self.ppa_data.get_domain_aliases(domain_info.name):
				for record in self.ppa_data.get_domain_alias_dns_records(alias.id):
					for subdomain_name in subdomain_names:
						assert(subdomain_name.endswith(u".%s" % (domain_info.name,)))
						subdomain_prefix = subdomain_name[0:-len(domain_info.name)-1]
						subdomain_on_alias_full_name = u"%s.%s" % (subdomain_prefix, alias.name)
						if record.src == u"%s." % (subdomain_on_alias_full_name,) and record.rec_type == 'A': # subdomain's record
							if record.dst == source_ip: # record has an old IP address
								self.logger.debug(u"Fix subdomain '%s' record in DNS zone of domain alias '%s'", subdomain_prefix, alias.name)
								self.ppa_data.delete_dns_record(record.id)
								self.ppa_data.add_domain_alias_dns_record(
									alias.id,
									src=subdomain_prefix, dst=target_ip, 
									rec_type=record.rec_type, opt=record.opt
								)

	def configure_apache_on_target(self, move_services, try_subscription):
		with self.conn.main_node_runner() as runner:
			for subscription in move_services.web:
				with try_subscription(subscription.name):
					if subscription.service_type == WebServiceType.Apache:
						try:
							# 1) Switch IP from old to new one in Plesk database
							self._run_plesk_move_subscription_command('change-subscription-web-ip', {'subscription-name': subscription.name, 'service-node-ip': subscription.target_ip})
							# 2) Re-create Apache configs on target node
							self._reconfigure_subscription_apache(runner, subscription.name)
						finally:
							# 3) Switch IP back from new to old one in Plesk database
							self._run_plesk_move_subscription_command('change-subscription-web-ip', {'subscription-name': subscription.name, 'service-node-ip': subscription.source_ip})
							# 4) Re-create Apache configs on source node
							self._reconfigure_subscription_apache(runner, subscription.name)

	def configure_iis_on_target(self, move_services, try_subscription):
		for subscription in move_services.web:
			with try_subscription(subscription.name):
				if subscription.service_type == WebServiceType.IIS:
					try:
						# 1) Switch IP from old to new one in Plesk database
						self._run_plesk_move_subscription_command('change-subscription-web-ip', {'subscription-name': subscription.name, 'service-node-ip': subscription.target_ip})
						# 2) Re-create IIS vhost configuration on target node
						self._run_plesk_move_subscription_command('recreate-iis-hosting', {'subscription-name': subscription.name})
					finally:
						# 3) Switch IP back from new to old one in Plesk database
						self._run_plesk_move_subscription_command('change-subscription-web-ip', {'subscription-name': subscription.name, 'service-node-ip': subscription.source_ip})

	def _reconfigure_subscription_apache(self, ppa_runner, subscription_name):
		site_names = [site.name for site in self.ppa_data.get_subscription_sites_info(subscription_name)]
		subdomain_names = self.ppa_data.get_subdomain_names([subscription_name] + site_names)
		product_root_d = get_unix_product_root_dir(ppa_runner)
		# apply to all domains, sites, subdomains
		# httpdmng --reconfigure-domain silently skips domains for which Apache configration is not applicable (domains without hosting)
		for vhost in [subscription_name] + site_names + subdomain_names:
			ppa_runner.run(u"%s/admin/bin/httpdmng" % (product_root_d,), ["--reconfigure-domain", vhost])

	def reconfigure_aps_applications(self, move_services, try_subscription):
		for subscription_name in set([subscription.name for subscription in move_services.web + move_services.db_mysql]):
			with try_subscription(subscription_name):
				self._run_plesk_move_subscription_command('reconfigure-aps-applications', {'subscription-name': subscription_name})

	def move_anonftp(self, move_services, try_subscription):
		ips = set()
		ips.update([subscription.source_ip for subscription in move_services.web]) # source IPs
		ips.update([subscription.target_service_node_ip for subscription in move_services.web]) # target IPs

		ips_by_service_nodes = defaultdict(list)
		for ip in ips:
			ips_by_service_nodes[self._get_host_id(ip)].append(ip)

		for _, host_ips in ips_by_service_nodes.iteritems():
			host_ip = host_ips[0] # take any IP address of the service node
			self._run_plesk_move_subscription_command('reconfigure-anonftp', {'service-node-ip': host_ip})

	def remove_logrotation(self, move_services, try_subscription):
		self._run_plesk_move_subscription_command_for_subscriptions_web_apache_source('remove-logrotation', move_services, try_subscription)
		# on IIS logrotation is stored in IIS site configuration, so there is no need to remove it and we need to run the command only for Apache

	def remove_webusers(self, move_services, try_subscription):
		self._run_plesk_move_subscription_command_for_subscriptions_web_source('remove-webusers', move_services, try_subscription)

	def remove_ftpusers(self, move_services, try_subscription):
		for subscription in move_services.web:
			with try_subscription(subscription.name):
				self._run_plesk_move_subscription_command('remove-ftpusers', {
					'subscription-name': subscription.name, 'service-node-ip': subscription.source_ip, 'source-subscription-ip': subscription.source_ip
				})

	def remove_sysusers(self, move_services, try_subscription):
		self._run_plesk_move_subscription_command_for_subscriptions_web_source('remove-sysuser', move_services, try_subscription)

	def remove_ssl_certificates(self, move_services, try_subscription):
		for source_ip in set([subscription.source_ip for subscription in move_services.web]):
			self._run_plesk_move_subscription_command('unbind-ssl-certificates', {'old-ip': source_ip})

	def remove_apache_config(self, move_services, try_subscription):
		subscriptions = self._get_web_apache_subscriptions(move_services)

		with self.conn.main_node_runner() as runner:
			for subscription in subscriptions:
				with try_subscription(subscription.name):
					self._run_plesk_move_subscription_command('remove-apache-config', {'subscription-name': subscription.name, 'service-node-ip': subscription.source_ip})
					self._reconfigure_subscription_apache(runner, subscription.name)

	def remove_iis_hosting(self, move_services, try_subscription):
		self._run_plesk_move_subscription_command_for_subscriptions_web_iis_source('remove-iis-hosting', move_services, try_subscription)

	@staticmethod
	def _get_web_apache_subscriptions(move_services):
		return [s for s in move_services.web if s.service_type == WebServiceType.Apache]

	def _run_plesk_move_subscription_command_for_subscriptions_web_target(self, command_name, move_services, try_subscription):
		for subscription in move_services.web:
			with try_subscription(subscription.name):
				self._run_plesk_move_subscription_command(command_name, {'subscription-name': subscription.name, 'service-node-ip': subscription.target_service_node_ip})

	def _run_plesk_move_subscription_command_for_subscriptions_web_iis_target(self, command_name, move_services, try_subscription):
		for subscription in move_services.web:
			if subscription.service_type == WebServiceType.IIS:
				with try_subscription(subscription.name):
					self._run_plesk_move_subscription_command(command_name, {'subscription-name': subscription.name, 'service-node-ip': subscription.target_service_node_ip})

	def _run_plesk_move_subscription_command_for_subscriptions_web_apache_target(self, command_name, move_services, try_subscription):
		for subscription in move_services.web:
			if subscription.service_type == WebServiceType.Apache:
				with try_subscription(subscription.name):
					self._run_plesk_move_subscription_command(command_name, {'subscription-name': subscription.name, 'service-node-ip': subscription.target_service_node_ip})

	def _run_plesk_move_subscription_command_for_subscriptions_mail_target(self, command_name, move_services, try_subscription):
		for subscription in move_services.mail:
			with try_subscription(subscription.name):
				self._run_plesk_move_subscription_command(command_name, {'subscription-name': subscription.name, 'service-node-ip': subscription.target_service_node_ip})

	def _run_plesk_move_subscription_command_for_subscriptions_web_source(self, command_name, move_services, try_subscription):
		for subscription in move_services.web:
			with try_subscription(subscription.name):
				self._run_plesk_move_subscription_command(command_name, {'subscription-name': subscription.name, 'service-node-ip': subscription.source_ip})

	def _run_plesk_move_subscription_command_for_subscriptions_web_apache_source(self, command_name, move_services, try_subscription):
		for subscription in move_services.web:
			if subscription.service_type == WebServiceType.Apache:
				with try_subscription(subscription.name):
					self._run_plesk_move_subscription_command(command_name, {'subscription-name': subscription.name, 'service-node-ip': subscription.source_ip})

	def _run_plesk_move_subscription_command_for_subscriptions_web_iis_source(self, command_name, move_services, try_subscription):
		for subscription in move_services.web:
			if subscription.service_type == WebServiceType.IIS:
				with try_subscription(subscription.name):
					self._run_plesk_move_subscription_command(command_name, {'subscription-name': subscription.name, 'service-node-ip': subscription.source_ip})

	def _run_plesk_move_subscription_command_for_subscriptions_mail_source(self, command_name, move_services, try_subscription):
		for subscription in move_services.mail:
			with try_subscription(subscription.name):
				self._run_plesk_move_subscription_command(command_name, {'subscription-name': subscription.name, 'service-node-ip': subscription.source_ip})

	def _run_plesk_move_subscription_command(self, cmd, kwargs={}):
		with self.conn.main_node_runner() as runner:
			product_root_d = get_unix_product_root_dir(runner)
			args = ['--%s' % cmd]
			for key, value in kwargs.iteritems():
				args.append('-%s' % key)
				args.append(value)
			runner.run(
				u"%s/bin/sw-engine-pleskrun" % (product_root_d,), 
				[u"%s/admin/plib/scripts/move_subscription.php" % product_root_d] + args
			)

	def _create_old_ip_limits(self):
		return OldIpLimitsFile(self.session_dir.get_file_path('old-ip-limits.yaml'))

	def _create_old_db_limits(self):
		return OldDbLimits(self.session_dir.get_file_path('old-db-limits.yaml'))

	def _read_move_list_lazy(self, options):
		if self.move_list is None:
			self.move_list = self._read_move_list(options)
		return self.move_list

	def _read_move_list(self, options):
		move_list_filename = self._get_move_list_filename(options)
		with open(move_list_filename, 'r') as f:
			subscriptions, errors = read_move_list(
				f, self.ppa_data, self.ppa_data.get_mysql_db_server_hosts()
			)
			if len(errors) == 0:
				return subscriptions
			else:
				raise MigrationError(u"Failed to read move list:\n%s\nPlease fix the move list file '%s' and restart moving tools." % (format_multiline_list(errors), move_list_filename))

	def _get_move_list_filename(self, options):
		if options.move_list_file is not None:
			move_list_file = options.move_list_file
		else:
			move_list_file = self.session_dir.get_file_path("move-list.xml")

		if not os.path.exists(move_list_file):
			raise MigrationError(
				u"Move list file does not exists. Please put correct move list file to '%s', or specify correct path to move list file with --move-list-file command-line option and restart moving tools." % move_list_file
			)

		self.logger.info(u"Move list from '%s' is used", move_list_file)
		return move_list_file
	
	def copy_mail_content(self, move_services, try_subscription):
		for subscription in move_services.mail:
			with try_subscription(subscription.name):
				self.logger.info(u"Copy mail content of '%s' subscription", subscription.name)
				subscription_info = self.ppa_data.get_subscription_info(subscription.name)
				source_ppa_host_id = get_host_id_by_ip(self.conn.poa_api(), subscription.source_ip)
				target_ppa_host_id = get_host_id_by_ip(self.conn.poa_api(), subscription.target_ip)
				with self.conn.ppa_unix_node_runner(source_ppa_host_id) as runner_source, self.conn.ppa_unix_node_runner(target_ppa_host_id) as runner_target:
					domains = self._get_sites_of_subscription(subscription_info, skip_subdomains=True)
					for domain_name, domain_kind in domains.iteritems():
						self.logger.info(u"Copy content of %s '%s'", domain_kind, domain_name)
						with ssh_utils.public_key_ssh_access_runner(runner_target, runner_source) as key_pathname:
							src_mailnames_path = plesk_utils.get_unix_mailnames_dir(runner_source)
							self.logger.debug(u"Target node mailnames path: %s", src_mailnames_path)
							
							dst_mailnames_path = plesk_utils.get_unix_mailnames_dir(runner_target)
							self.logger.debug(u"Target node mailnames path: %s", dst_mailnames_path)
							
							runner_target.run(u'/usr/bin/rsync', [
								u"-a", u"-e", u"ssh -i %s -o StrictHostKeyChecking=no -o GSSAPIAuthentication=no" % key_pathname,
								u"--exclude", u"maildirsize", u"--exclude", u".qmail*", u"--exclude", u"@attachments/",
								u"%s@%s:%s/%s/" % ('root', subscription.source_ip, src_mailnames_path, domain_name.encode('idna')),
								u"%s/%s" % (dst_mailnames_path, domain_name.encode('idna'))
							])

	def copy_mail_autoresponder_attachments(self, move_services, try_subscription):
		for subscription in move_services.mail:
			with try_subscription(subscription.name):
				self._run_plesk_move_subscription_command('copy-mail-autoresponder-attachments', {
					'subscription-name': subscription.name, 
					'target-service-node-ip': subscription.target_service_node_ip, 
					'source-service-node-ip': subscription.source_ip
				})

class SubscriptionIPv4Limit:
	def __init__(self, poa_api, subscription_name):
		self.poa_api = poa_api
		self.subscription_name = subscription_name
		self.subscription_poa_id = None
		self.ipv4_rt = self._get_ipv4_rt()

	def fetch(self):
		# if there is no resource, it is shared ip
		if self.ipv4_rt is None:
			return 0
		else:
			return int(self.ipv4_rt.limit)

	def set(self, value):
		if self.ipv4_rt is None:
			# value equals 0 means ip is shared and don't need to change
			if value == 0:
				pass
			else:
				raise MigrationError(
					u"Subscription does not have any IPv4 resource"
				)
		else:
			self.poa_api.set_resource_type_limits(
				subscription_id=self._get_subscription_poa_id_lazy(),
				limits=[{'resource_type_id': self.ipv4_rt.rt_id, 'limit': str(value)}]
			)

	def _get_ipv4_rt(self):
		return find_first(
			self.poa_api.getSubscription(self._get_subscription_poa_id_lazy(), get_resources=True).resources,
			lambda rt: rt.resclass_name == 'ips'
		)

	def _get_subscription_poa_id_lazy(self):
		if self.subscription_poa_id is None:
			webspace_id = self.poa_api.get_webspace_id_by_primary_domain(self.subscription_name)
			self.subscription_poa_id = self.poa_api.get_webspace(webspace_id)['sub_id']
		return self.subscription_poa_id

