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

from parallels.ppa_sn import messages
from parallels.core.utils import migrator_utils, ssh_utils, plesk_utils
from parallels.core.utils.config_utils import ConfigSection
from parallels.core.registry import Registry
from parallels.ppa.connections.target_connections import PPATargetConnections
from parallels.core import checking
from parallels.core.logging_context import log_context, subscription_context
from parallels.ppa.utils.poa_utils import get_host_id_by_ip
from parallels.core.utils.plesk_utils import get_unix_vhost_dir, get_unix_product_root_dir
from parallels.core.utils.common import format_list, format_multiline_list, group_by_id, find_only, find_first, exists
from parallels.core.cli.common_cli import CommandTypes
from parallels.core.utils.migrator_utils import format_report, save_report
from parallels.core 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(messages.START_MIGRATOR % (sys.argv[0], " ".join("'%s'" % arg for arg in sys.argv[1:])))
		self.logger.info(messages.LOG_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(messages.COPY_CONTENT_OF_UNIX_SUBSCRIPTIONS)
		for subscription in subscriptions:
			with try_subscription(subscription.name):
				self.logger.info(messages.COPY_CONTENT_OF_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(messages.LOG_COPY_CONTENT, 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(messages.ATTEMPT_TO_REMOVE_VHOST_WITH_EMPTY_DOMAIN_NAME)

		for subscription in move_services.web:
			with try_subscription(subscription.name):
				self.logger.info(messages.REMOVE_CONTENT_OF_SUBSCRIPTION_FROM_SOURCE, 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(messages.LOG_REMOVE_CONTENT, 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(messages.DOMAIN_SHOULD_NOT_HAVE_CONTENT, 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(messages.SUMMARY)
		self.logger.info(
			messages.OPERATION_FINISHED_SUCCESSFULLY_STATS,
			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=messages.MOVE_SUBSCRIPTION_TO_TARGET_NODE_IN_SINGLE_STEP,
			subcommands=[
				Command(
					name='copy-hosting', command_type=CommandTypes.MACRO,
					brief_description=messages.COPY_WEB_HOSTING_TO_TARGET_NODE,
					subcommands=[
						Command(
							name='check', command_type=CommandTypes.MACRO,
							brief_description=messages.CHECK_FOR_POTENTIAL_MOVE_SUBSCRIPTION_ISSUES,
							log_description=messages.CHECK_FOR_MOVE_SUBSCRIPTION_ISSUES_LOG_DESCRIPTION,
						),
						Command(
							name='increase-web-ip-limits', command_type=CommandTypes.MICRO,
							brief_description=messages.INCREASE_IP_LIMITS_OF_SUBSCRIPTIONS,
							log_description=messages.INCREASE_IP_LIMITS_OF_SUBSCRIPTIONS_LOG_DESCRIPTION,
						),
						Command(
							name='allocate-web-ip', command_type=CommandTypes.MICRO,
							brief_description=messages.ALLOCATE_IP,
							log_description=messages.ALLOCATE_IP_ON_TARGET_WEB_SERVICE,
						),
						Command(
							name='allocate-mail-ip', command_type=CommandTypes.MICRO,
							brief_description=messages.ALLOCATE_IP_ON_TARGET_MAIL_SERVICE,
							log_description=messages.ALLOCATE_IP_ON_TARGET_MAIL_SERVICE_LOG_DESCRIPTION,
						),
						Command(
							name='allocate-db-ip', command_type=CommandTypes.MICRO,
							brief_description=messages.ALLOCATE_IP_ON_TARGET_DATABASE_SN,
							log_description=messages.ALLOCATE_IP_ON_TARGET_DATABASE_SN_LOG_DESCRIPTION,
						),
						Command(
							name='recreate-sysuser', command_type=CommandTypes.MICRO,
							brief_description=messages.RECREATE_SYSTEM_USERS_ON_TARGET_NODE,
							log_description=messages.RECREATE_SYSTEM_USERS_ON_TARGET_NODE_LOG_DESCRIPTION,
						),
						Command(
							name='configure-iis-on-target',
							command_type=CommandTypes.MICRO,
							brief_description=messages.CONFIGURE_IIS_ON_TARGET_NODE,
							log_description=messages.LOG_DESCRIPTION_CONFIGURE_IIS_ON_TARGET_NODE,
						),
						Command(
							name='recreate-cronjobs', command_type=CommandTypes.MICRO,
							brief_description=messages.RECREATE_CRON_JOBS_ON_TARGET_NODE,
							log_description=messages.RECREATE_CRON_JOBS_ON_TARGET_NODE_LOG_DESCRIPTION,
						),
						Command(
							name='recreate-webusers', command_type=CommandTypes.MICRO,
							brief_description=messages.RECREATE_WEB_USERS_ON_TARGET,
							log_description=messages.RECREATE_WEB_USERS_ON_TARGET_NODE_LOG_DESCRIPTION,
						),
						Command(
							name='recreate-ftpusers', command_type=CommandTypes.MICRO,
							brief_description=messages.RECREATE_FTP_USERS_ON_TARGET_NODE,
							log_description=messages.RECREATE_FTP_USERS_ON_TARGET_NODE_LOG_DESCRIPTION,
						),
						Command(
							name='recreate-ssl-certificates', command_type=CommandTypes.MICRO,
							brief_description=messages.RECREATE_SSL_CERTIFICATES_ON_TARGET_NODE,
							log_description=messages.RECREATE_SSL_CERTIFICATES_ON_TARGET_NODE_LOG_DESCRIPTION,
						),
						Command(
							name='recreate-logrotation', command_type=CommandTypes.MICRO,
							brief_description=messages.RECREATE_LOGROTATION_CONFIGURATION_ON_TARGET,
							log_description=messages.RECREATE_LOGROTATION_CONFIG_ON_TARGET_NODE_LOG_DESCRIPTION,
						),
						Command(
							name='recreate-mail', command_type=CommandTypes.MICRO,
							brief_description=messages.RECREATE_MAIL_OBJECTS_ON_TARGET_NODE,
							log_description=messages.RECREATE_MAIL_OBJECTS_ON_TARGET_NODE_LOG_DESCRIPTION),
						Command(
							name='copy-web-content', command_type=CommandTypes.MICRO,
							brief_description=messages.COPY_WEB_CONTENT_OF_SUBSCRIPTION,
							log_description=messages.COPY_WEB_CONTENT,
						),
						Command(
							name='recreate-protected-dirs',
							command_type=CommandTypes.MICRO,
							brief_description=messages.RECREATE_PROTECTED_DIRECTORIES,
							log_description=messages.RECREATE_PROTECTED_DIRECTORIES_LOG_DESCRIPTION,
						),
						Command(
							name='recreate-virtual-dirs',
							command_type=CommandTypes.MICRO,
							brief_description=messages.RECREATE_VDIRS,
							log_description=messages.LOG_DESCRIPTION_RECREATE_VDIRS,
						),
						Command(
							name='recreate-iis-hosting-settings',
							command_type=CommandTypes.MICRO,
							brief_description=messages.RECREATE_IIS_SETTINGS,
							log_description=messages.LOG_DESCRIPTION_RECREATE_IIS_SETTINGS,
						),
						Command(
							name='copy-mail-content', command_type=CommandTypes.MICRO,
							brief_description=messages.COPY_MAIL_CONTENT_OF_SUBSCRIPTION_TO_TARGET,
							log_description=messages.COPY_MAIL_CONTENT),
						Command(
							name='copy-mail-autoresponder-attachments', command_type=CommandTypes.MICRO,
							brief_description=messages.COPY_MAIL_AUTORESPONDER_ATTACHMENTS,
							log_description=messages.COPY_MAIL_AUTORESPONDER_ATTACHMENTS_LOG_DESCRIPTION),
						Command(
							name='copy-databases', command_type=CommandTypes.MICRO,
							brief_description=messages.COPY_DATABASES,
							log_description=messages.COPY_DATABASES_FROM_SOURCE_TO_TARGET,
						),
						Command(
							name='recreate-chrooted-shell', command_type=CommandTypes.MICRO,
							brief_description=messages.RECREATE_CHROOTED_SHELL_FOR_SYS_USERS, 
							log_description=messages.RECREATE_CHROOTED_SHELL_FOR_SYS_USERS_LOG_DESCRIPTION,
						),
					]
				),
				Command(
					name='configure-apache-on-target',
					command_type=CommandTypes.MACRO,
					brief_description=messages.CONFIGURE_APACHE_ON_TARGET_NODE,
					log_description=messages.CONFIGURE_APACHE_ON_TARGET,
				),
				Command(
					name='change-subscription-ip', command_type=CommandTypes.MACRO,
					brief_description=messages.CHANGE_SUBSCRIPTIONS_IP_ADDRESSES,
					subcommands=[
						Command(
							name='change-subscription-web-ip', command_type=CommandTypes.MICRO,
							brief_description=messages.CHANGE_SUBSCRIPTIONS_WEB_IP_ADDRESS,
							log_description=messages.CHANGE_SUBSCRIPTIONS_WEB_IP_ADDRESS_IN_PPA_DB,
						),
						Command(
							name='change-subscription-mail-ip', command_type=CommandTypes.MICRO,
							brief_description=messages.CHANGE_SUBSCRIPTIONS_MAIL_IP_ADDRESS,
							log_description=messages.CHANGE_SUBSCRIPTIONS_MAIL_IP_ADDRESS_LOG_DESCRIPTION,
						),
						Command(
							name='change-subscription-db-ip', command_type=CommandTypes.MICRO,
							brief_description=messages.CHANGE_SUBSCRIPTIONS_DATABASES_IP_ADDRESS,
							log_description=messages.CHANGE_SUBSCRIPTIONS_DB_IP_ADDRESS_LOG_DESCRIPTION,
						),
					]
				),
				Command(
					name='move-aps-applications',
					command_type=CommandTypes.MACRO,
					brief_description=messages.MOVE_APS_APPLICATIONS,
					subcommands=[
						Command(
							name='reassign-aps-db-resources', command_type=CommandTypes.MICRO,
							brief_description=messages.REASSIGN_APS_DATABASE_RESOURCES, 
							log_description=messages.REASSIGN_APS_DATABASE_RESOURCES_LOG_DESCRIPTION,
						),
						Command(
							name='reconfigure-aps-applications', command_type=CommandTypes.MICRO,
							brief_description=messages.RECONFIGURE_APS_APPLICATIONS_ON_TARGET_NODE, 
							log_description=messages.RECONFIGURE_APS_APPLICATIONS_ON_TARGET,
						),
					]
				),
				Command(
					name='reconfigure-dns', command_type=CommandTypes.MACRO,
					brief_description=messages.RECONFIGURE_DNS,
					log_description=messages.LOG_DESCRIPTION_RECONFIGURE_DNS,
				),
				Command(
					name='remove-from-source',
					command_type=CommandTypes.MACRO,
					brief_description=messages.REMOVE_HOSTING_DATA_FROM_SOURCE_NODE,
					subcommands=[
						Command(
							name='remove-apache-config', command_type=CommandTypes.MICRO,
							brief_description=messages.REMOVE_APACHE_VHOSTS_CONFIGURATION_FROM_SOURCE,
							log_description=messages.REMOVE_APACHE_VHOSTS_CONFIG_FROM_SOURCE_LOG_DESCRIPTION,
						),
						Command(
							name='move-anonftp', command_type=CommandTypes.MICRO,
							brief_description=messages.MOVE_ANONYMOUS_FTP_FROM_SOURCE_NODE,
							log_description=messages.MOVE_ANONYMOUS_FTP,
						),
						Command(
							name='remove-logrotation', command_type=CommandTypes.MICRO,
							brief_description=messages.REMOVE_LOGROTATION_CONFIG_FROM_SOURCE,
							log_description=messages.REMOVE_LOGROTATION_CONFIG_FROM_SOURCE_LOG_DESCRIPTION,
						),
						Command(
							name='remove-webusers',	command_type=CommandTypes.MICRO,
							brief_description=messages.REMOVE_WEB_USERS_FROM_SOURCE_NODE,
							log_description=messages.REMOVE_WEB_USERS_FROM_SOURCE_NODE_LOG_DESCRIPTION,
						),
						Command(
							name='remove-ftpusers',	command_type=CommandTypes.MICRO,
							brief_description=messages.REMOVE_FTP_USERS_FROM_SOURCE_NODE,
							log_description=messages.REMOVE_FTP_USERS_FROM_SOURCE_NODE_LOG_DESCRIPTION,
						),
						Command(
							name='remove-ssl-certificates', command_type=CommandTypes.MICRO,
							brief_description=messages.REMOVE_SSL_CERTIFICATES_FROM_SOURCE_NODE,
							log_description=messages.REMOVE_SSL_CERTIFICATES_FROM_SOURCE_LOG_DESCRIPTION,
						),
						Command(
							name='remove-web-content', command_type=CommandTypes.MICRO,
							brief_description=messages.REMOVE_WEB_CONTENT,
							log_description=messages.REMOVE_WEB_CONTENT_FROM_SOURCE_NODE,
						),
						Command(
							name='remove-sysusers', command_type=CommandTypes.MICRO,
							brief_description=messages.REMOVE_SYSTEM_USERS_AND_CONTENT,
							log_description=messages.REMOVE_SYSTEM_USERS_AND_CONTENT_LOG_DESCRIPTION,
						),
						Command( # must follow remove webusers
							name='remove-iis-hosting', command_type=CommandTypes.MICRO,
							brief_description=messages.REMOVE_IIS_VIRTUAL_HOSTS_FROM_SOURCE,
							log_description=messages.REMOVE_IIS_VIRTUAL_HOSTS_FROM_SOURCE_LOG_DESCRIPTION,
						),
						Command(
							name='remove-databases', command_type=CommandTypes.MICRO,
							brief_description=messages.REMOVE_DATABASES_FROM_SOURCE_NODE, 
							log_description=messages.REMOVE_DATABASES_FROM_SOURCE_NODE_LOG_DESCRIPTION,
						),
						Command(
							name='remove-mail', command_type=CommandTypes.MICRO,
							brief_description=messages.REMOVE_MAIL_FROM_SOURCE,
							log_description=messages.LOG_DESCRIPTION_REMOVE_MAIL_FROM_SOURCE,
						),
						Command(
							name='free-old-subscription-web-ips', command_type=CommandTypes.MICRO,
							brief_description=messages.FREE_OLD_SUBSCRIPTION_WEB_IPS,
							log_description=messages.FREE_OLD_SUBSCRIPTION_WEB_IPS_LOG_DESCRIPTION),
						Command(
							name='free-old-subscription-mail-ips', command_type=CommandTypes.MICRO,
							brief_description=messages.FREE_OLD_SUBSCRIPTION_MAIL_IPS,
							log_description=messages.FREE_OLD_SUBSCRIPTION_MAIL_IPS_LOG_DESCRIPTION),
						Command(
							name='restore-web-ip-limits', command_type=CommandTypes.MICRO,
							brief_description=messages.RESTORE_OLD_IP_LIMITS_OF_SUBSCRIPTIONS,
							log_description=messages.RESTORE_OLD_IP_LIMITS_OF_SUBSCRIPTIONS_LOG_DESCRIPTION)
					]
				),
			]
		)

	@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(
				messages.EXECUTE_COMMAND_FOR_SUBSCRIPTIONS, 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(messages.START_COMMAND, command.log_description)

				cls.logger.debug(messages.DEBUG_IGNORE_EXECUTION_STATUS, 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(
						messages.LOG_SKIP_FOR_SUBSCRIPTIONS_FAILED, format_list(subscriptions_skip_previous_failed)
					)
				if len(subscriptions_skip_already_executed) > 0:
					cls.logger.info(
						messages.LOG_SKIP_SUBSCRIPTIONS_ALREADY_EXECUTED, format_list(subscriptions_skip_already_executed)
					)
				if len(subscriptions_to_execute) > 0:
					cls.logger.info(messages.LOG_EXECUTE_FOR_SUBSCRPTIONS, 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(messages.FINISH_COMMAND, 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(messages.ALL_SUBSCRIPTIONS_ARE_FAILED_TO_MIGRATE)
						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(messages.FAILED_TO_PERFORM_AN_ACTION_ON_SUBSCRIPTION, name, e)
				cls.logger.debug(messages.LOG_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(messages.FAILED_TO_PERFORM_ACTION_FOR_SUBSCRIPTIONS, format_list(subscription_names), e)
			cls.logger.debug(messages.LOG_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(messages.REPORT_DETECTED_POTENTIAL_ISSUES, None)
		db_server_id_by_name = self.ppa_data.get_db_server_id_by_host()
		self.logger.info(messages.LOG_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(messages.CHECK_THAT_DATABASE_ALREADY_EXISTS)
				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(messages.DATABASE_ALREADY_EXISTS, db.name, subscription.target_host)
						subreport.add_issue(
							checking.Problem(
								'mysql-migration-check',
								checking.Problem.WARNING, messages.DATABASE_ALREADY_EXISTS % (
									db.name, subscription.target_host
								)
							),
							messages.CAUSED_BY_PREVIOUS_MOVING
						)
						self.logger.debug(messages.CHECK_THAT_DATABASE_USERS_ALREADY_EXISTS)
						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, 
											messages.DATABASE_USER_FOR_DATABASE_ALREADY_EXISTS % (
												source_db_user.login, db.name, subscription.target_host
											)
										),
										messages.CAUSED_BY_PREVOUS_MOVE)

		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, 
							messages.SUBSCRIPTION_HAS_NO_WEB_HOSTING),
						messages.REMOVE_WEB_NODE_SOLUTION)
		
		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(messages.REPORT_WAS_SAVED_INTO_FILE % path)
		if report.has_errors():
			raise MigrationError(messages.UNABLE_TO_CONTINUE_SUBSCRIPTION_MOVING)
	
	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(
						messages.IP_ADDRESS_WAS_ALREADY_ALLOCATED, 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(messages.LOG_CREATE_DATABASE_ON_HOST, 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(messages.DATABASE_ALREADY_EXISTS, db.name, subscription.target_host)

				self.logger.debug(messages.DEBUG_CLONE_DB_USERS)
				self._run_plesk_move_subscription_command(
					'clone-mysql-users', {'subscription-name': subscription.name, 'db-host': subscription.target_host}
				)

				self.logger.debug(messages.SET_DATABASE_ADMINISTRATOR_FLAG_FOR_DB_USERS)
				# re-fetch databases list to get cloned databases from the target server
				databases = self.ppa_data.get_mysql_databases(subscription.name)
				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, 
								messages.UNABLE_TO_FIND_SOURCE_DB_ADMIN).login
							target_db_admin_id = find_only(
								db_users[target_db_id], lambda user: user.login == source_db_admin_login,
								messages.UNABLE_TO_FIND_TARGET_DATABASE_USER).id
							self.ppa_data.set_db_admin_user_id(target_db_id, target_db_admin_id)
						else:
							# there is no database administrator for this database,
							# so there is nothing to change on the target
							pass

		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(messages.COPY_CONTENT_OF_DATABASE_TO_SERVER, 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(messages.SKIP_COPY_CONTENT_OF_DATABASE % (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 '
				u'--quote-names --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(
					messages.PG_DUMP_UTILITY_IS_NOT_INSTALLED % src_server_ip
				)
		else:
			raise Exception(messages.DATABASE_OF_UNSUPPORTED_TYPE % 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 '
				u'-o GSSAPIAuthentication=no {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(
						messages.NOT_ALL_DATABASES_WERE_COPIED % (
							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(
				messages.DATABASES_WITH_SAME_NAME % (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(messages.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(messages.DEBUG_CHANGE_SUBSCRIPTION_IP_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(messages.DEBUG_CHANGE_SUBSCRIPTION_IP_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(messages.CHANGE_SUBSCRIPTIONS_DEFAULT_MYSQL_SERVER)
				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(messages.CHANGE_SUBSCRIPTION_IP_IN_PLESK_LOG_DESCRIPTION)
				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(messages.DEBUG_CHANGE_SUBSCRIPTION_IP_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
				)

	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(
									messages.FIX_SUBDOMAIN_RECORD_IN_DNS_ZONE, 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():
			# take any IP address of the service node
			host_ip = host_ips[0]
			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(
					messages.FAILED_TO_READ_MOVE_LIST % (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(
				messages.MOVE_LIST_FILE_DOES_NOT_EXISTS % move_list_file
			)

		self.logger.info(messages.LOG_MOVE_LIST_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(messages.COPY_MAIL_CONTENT_OF_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(messages.LOG_COPY_MAIL_CONTENT, 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(messages.DEBUG_SOURCE_MAIL_PATH, src_mailnames_path)
							
							dst_mailnames_path = plesk_utils.get_unix_mailnames_dir(runner_target)
							self.logger.debug(messages.DEBUG_TARGET_MAIL_PATH, 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(messages.SUBSCRIPTION_DOES_NOT_HAVE_ANY_IPV4)
		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