import os
import re
import logging
from collections import namedtuple, defaultdict
from contextlib import contextmanager
import socket
import itertools

from parallels.core.utils.entity import Entity
from parallels.core.utils import unix_utils, migrator_utils, windows_utils, database_utils
from parallels.core.utils import unique_name_generator
from parallels.core.checking import Problem
from parallels.core import checking
from parallels.core.migrator_config import MailImapEncryption
from parallels.core.utils.database_utils import DatabaseServerConnectionException
from parallels.core.windows_rsync import RsyncClientNonZeroExitCode

from parallels.core.utils.common import format_list, group_by, safe_string_repr, obj
from parallels.core.powershell import Powershell

import parallels.plesk.source.plesk
from parallels.plesk.source.plesk import messages

NodesPair = namedtuple('NodesPair', ('source', 'target'))
SubscriptionsInfo = namedtuple('SubscriptionsInfo', (
	'subscription_name',
	'subscription_dir',
	'target_node',
	'source_ip'
))

logger = logging.getLogger(__name__)


class InfrastructureChecks(object):
	"""Check disk space and connections, format the report."""
	def __init__(self):
		self.checks_cache = {}

	def check_copy_content(self, check_type, subscriptions, report, check_function, content_type):
		result = []
		for nodes_pair_info, nodes_subscriptions in subscriptions:
			success, reason = self._check_cached(check_type, check_function, nodes_pair_info)
			result.append((nodes_subscriptions, success, reason))

		if content_type == 'web':
			self._format_copy_web_content_report(report, result)
		elif content_type == 'mail':
			self._format_copy_mail_content_report(report, result)

	def check_copy_db_content(self, check_type, subscriptions, report, check_function):
		result = []
		for nodes_pair_info, nodes_subscriptions in subscriptions:
			success, reason = self._check_cached(check_type, check_function, nodes_pair_info)
			result.append((nodes_subscriptions, success, reason))

		self._format_copy_db_content_report(report, result)

	def check_mssql_connection(self, subscriptions, report, check_function):
		result = []
		for (nodes_pair_info, mssql_connection_data), databases in subscriptions:
			success, reason = check_function(nodes_pair_info, mssql_connection_data)
			result.append((databases, success, reason))

		self._format_copy_db_content_report(report, result)

	def check_windows_mail_source_connection(self, subscriptions_by_source, report, checker):
		result = []
		for node, subscriptions in subscriptions_by_source.iteritems():
			success, reason = checker.check(node)
			result.append((subscriptions, success, reason))

		self._format_copy_mail_content_report(report, result)

	def check_windows_mail_target_connection(self, subscriptions_by_target, report, checker):
		result = []
		for node, subscriptions in subscriptions_by_target.iteritems():
			success, reason = checker.check(node)
			result.append((subscriptions, success, reason))

		self._format_copy_mail_content_report(report, result)

	def _check_cached(self, check_type, check_function, nodes_pair_info):
		if not (check_type, nodes_pair_info) in self.checks_cache:
			self.checks_cache[(check_type, nodes_pair_info)] = check_function(nodes_pair_info)
		return self.checks_cache[(check_type, nodes_pair_info)]

	@staticmethod
	def _format_copy_db_content_report(report, check_result):
		def format_databases(db_infos):
			db_type_pretty_name = {'mysql': 'MySQL', 'postgresql': 'PostgreSQL', 'mssql': 'MSSQL'}

			return ", ".join(
				"'%s' (%s)" % (
					subscription, 
					", ".join([
						"%s database(s): %s" % (db_type_pretty_name.get(db_type, 'unknown'), format_list([d['db_name'] for d in databases])) 
						for db_type, databases in group_by(subscription_databases, lambda d: d['db_type']).iteritems()
					])
				)
				for subscription, subscription_databases in group_by(db_infos, lambda d: d['subscription']).iteritems()
			)

		for databases, success, reason in check_result:
			if not success:
				report.add_issue(
					Problem(
						'infrastructure-databases', Problem.ERROR, 
						messages.INFRASTRUCTURE_CHECK_DB_ISSUES % (reason, format_databases(databases))
					), u""
				)
			else:
				report.add_issue(Problem('infrastructure-unix-databases', Problem.INFO, reason), u"")

	@classmethod
	def _format_copy_web_content_report(cls, report, check_result):
		cls._format_copy_content_report(report, check_result, issue_id='infrastructure-web', object_name='web content')

	@classmethod
	def _format_copy_mail_content_report(cls, report, check_result):
		cls._format_copy_content_report(report, check_result, issue_id='infrastructure-mail', object_name='mail messages')

	@staticmethod
	def _format_copy_content_report(report, check_result, issue_id, object_name):
		for subscriptions, success, reason in check_result:
			if not success:
				report.add_issue(
					Problem(
						issue_id, Problem.ERROR, 
						messages.INFRASTRUCTURE_CHECK_CONTENT_ISSUES % (reason, object_name, format_list(subscriptions))
					), u""
				)
			else:
				report.add_issue(Problem(issue_id, Problem.INFO, reason), u"")


def check_main_node_disk_requirements(runner, subscriptions_count):
	MB = 1024 * 1024
	GB = 1024 * MB

	required = defaultdict(int)
	available = {}

	var_device, var_available = _get_available_disk_space(runner, '/var')
	available[var_device] = var_available
	usr_device, usr_available = _get_available_disk_space(runner, '/usr')
	available[usr_device] = usr_available

	required[var_device] += 1 * GB + 10 * MB * subscriptions_count
	required[usr_device] += 1 * GB + 10 * MB * subscriptions_count

	logger.debug(messages.DEBUG_DISK_SPACE_CHECK, subscriptions_count, dict(required), dict(available))
	checks_passed = True
	for device, device_required in required.iteritems():
		if device_required > available[device]:
			logger.error(messages.LACK_OF_DISK_SPACE_ON_DEVICE.format(
				required=_format_bytes(device_required), device=device, available=_format_bytes(available[device])
			))
			checks_passed = False
	
	return checks_passed


class FileCopyBetweenNodesChecker(object):
	_test_file_contents = 'test-file-contents'

	@staticmethod
	def get_command_name():
		"""Override in child classes"""
		raise NotImplementedError()

	@staticmethod
	def _get_nodes_filepath(nodes):
		"""
		Generate full path to test file on both source and target nodes using provided file name template

		:type nodes: NodesPair
		:rtype: tuple
		"""
		filename = unique_name_generator.get_unique_filename(prefix='check-rsync-testfile-')
		source_filename = filename + '.source'
		target_filename = filename + '.target'

		source_temp_filepath = nodes.source.get_session_file_path(source_filename)
		target_temp_filepath = nodes.target.get_session_file_path(target_filename)

		# source_filename is returned due to migrator rsync specific configuration
		return source_temp_filepath, target_temp_filepath, source_filename


class WindowsFileCopyBetweenNodesRsyncChecker(FileCopyBetweenNodesChecker):
	def check(self, nodes, get_rsync):
		"""
		:type nodes: NodesPair
		:type get_rsync: function

		:return:
			1st element: True if check passed successfully, False otherwise
			2nd element: check result message
		:rtype: tuple (bool, basestring)
		"""

		message_formatter = CopySampleFileMessageFormatter(nodes)
		logger.info(message_formatter.format_check_log_message(self.get_command_name()))

		source_temp_filepath, target_temp_filepath, filename = self._get_nodes_filepath(nodes)
		with nodes.source.runner() as runner_source:
			runner_source.upload_file_content(source_temp_filepath, self._test_file_contents)

		try:
			rsync = get_rsync(nodes.source, nodes.target)
			rsync.sync(
				source_path='migrator/%s' % filename,
				target_path=windows_utils.convert_path_to_cygwin(target_temp_filepath),
			)
		except RsyncClientNonZeroExitCode as e:
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)

			return Result.failure(message_formatter.format_failure(
				command_name=self.get_command_name(), 
				problem_description=message_formatter.format_windows_rsync_problem_description(),
				failed_command_description=message_formatter.format_command_description(
					command_str=e.command, 
					exit_code=e.exit_code, 
					stdout=e.stdout, 
					stderr=e.stderr
				)
			))

		# remove file only if check finished successfully, so customer could try to run
		# the command himself to check what's wrong with configuration
		with nodes.target.runner() as runner_target:
			runner_target.remove_file(target_temp_filepath)
		with nodes.source.runner() as runner_source:
			runner_source.remove_file(source_temp_filepath)

		return Result.success(message_formatter.format_success(command_name=self.get_command_name()))

	@staticmethod
	def get_command_name():
		return 'rsync'


class UnixFileCopyBetweenNodesChecker(FileCopyBetweenNodesChecker):
	def check(self, nodes, ssh_key_pool):
		"""
		:type nodes: NodesPair
		:type ssh_key_pool: parallels.core.utils.ssh_key_pool.SSHKeyPool

		:return:
			1st element: True if check passed successfully, False otherwise
			2nd element: check result message
		:rtype: tuple (bool, basestring)
		"""
		message_formatter = CopySampleFileMessageFormatter(nodes)
		logger.info(message_formatter.format_check_log_message(self.get_command_name()))

		source_temp_filepath, target_temp_filepath, _ = self._get_nodes_filepath(nodes)

		with nodes.source.runner() as runner_source:
			runner_source.upload_file_content(source_temp_filepath, self._test_file_contents)
		key_pathname = ssh_key_pool.get(nodes.source, nodes.target).key_pathname

		with \
			nodes.source.runner() as runner_source, \
			nodes.target.runner() as runner_target:
			cmd, args = self._create_command(
				key_pathname, 
				runner_source, nodes.source.user(), nodes.source.ip(), 
				source_temp_filepath, target_temp_filepath, nodes.source.settings().ssh_auth.port
			)
			exit_code, stdout, stderr = runner_target.run_unchecked(cmd, args)

			command_description = message_formatter.format_command_description(
				command_str=unix_utils.format_command_list(cmd, args),
				exit_code=exit_code, stdout=stdout, stderr=stderr
			)

			if exit_code != 0:
				problem_description = messages.UNIX_COPY_FILE_CONNECTION_FAILED_SOLUTION.format(
					ssh_port=nodes.source.settings().ssh_auth.port
				)

				return Result.failure(message_formatter.format_failure(
					command_name=self.get_command_name(),
					problem_description=problem_description,
					failed_command_description=command_description
				))

			target_file_contents = runner_target.get_file_contents(target_temp_filepath)
			if target_file_contents != self._test_file_contents:
					problem_description = messages.INFRASTRUCTURE_INVALID_FILE_CONTENT % (
						self.get_command_name(), self._test_file_contents, target_file_contents
					)
					return Result.failure(message_formatter.format_failure(
						command_name=self.get_command_name(),
						problem_description=problem_description,
						failed_command_description=command_description
					))

			# remove file only if check finished successfully,
			# so customer could try to run the command himself to check what's wrong with configuration
			runner_target.remove_file(target_temp_filepath)
			runner_source.remove_file(source_temp_filepath)

		return Result.success(message_formatter.format_success(command_name=self.get_command_name()))

	@staticmethod
	def _create_command(
		key_pathname, source_runner, source_user, source_ip, source_temp_filepath, target_temp_filepath,
		source_port
	):
		"""Override in child classes"""
		raise NotImplementedError()


class UnixFileCopyBetweenNodesRsyncChecker(UnixFileCopyBetweenNodesChecker):
	@staticmethod
	def get_command_name():
		return 'rsync'

	@staticmethod
	def _create_command(
		key_pathname, source_runner, source_user, source_ip, source_temp_filepath, target_temp_filepath, source_port
	):
		return migrator_utils.create_rsync_command(
			key_pathname=key_pathname, source_runner=source_runner, 
			source_user=source_user, source_ip=source_ip, 
			source_filepath=source_temp_filepath, target_filepath=target_temp_filepath,
			source_port=source_port
		)


class UnixFileCopyBetweenNodesScpChecker(UnixFileCopyBetweenNodesChecker):
	@staticmethod
	def get_command_name():
		return 'scp'

	@staticmethod
	def _create_command(
		key_pathname, source_runner, source_user, source_ip, source_temp_filepath, target_temp_filepath, source_port
	):
		cmd = '/usr/bin/scp'
		args = [
			'-i', key_pathname, '-P', str(source_port),
			'-o', 'StrictHostKeyChecking=no', '-o', 'GSSAPIAuthentication=no',
			u"{source_user}@{source_ip}:{source_filename}".format(
				source_user=source_user, 
				source_ip=source_ip, 
				source_filename=source_temp_filepath
			),
			target_temp_filepath,
		]
		return cmd, args


class MysqlMaxAllowedPacketChecker(object):
	@classmethod
	def check(cls, source_database_server, target_database_server, report):
		"""Check source and target max allowed packet for mysql database server.

		:type source_database_server: parallels.core.connections.database_server.DatabaseServer
		:type target_database_server: parallels.core.connections.database_server.DatabaseServer
		"""

		failures = []
		source_db_max_allowed_packet = cls._get_source_mysql_max_allowed_packet(source_database_server)
		target_db_max_allowed_packet = cls._get_target_mysql_max_allowed_packet(target_database_server)
		
		logger.debug(
			messages.LOG_MAX_ALLOWED_MYSQL_PACKET,
			source_database_server.description(), source_db_max_allowed_packet
		)
		logger.debug(
			messages.LOG_MAX_ALLOWED_MYSQL_PACKET,
			target_database_server.description(), target_db_max_allowed_packet
		)

		if source_db_max_allowed_packet > target_db_max_allowed_packet:
			failures.append((
				source_db_max_allowed_packet,
				target_db_max_allowed_packet,
				source_database_server.description(),
				target_database_server.description()
			))
		cls.make_report(failures, target_database_server.description(), report)

	@staticmethod
	def make_report(failures, description_target_database, report):
		if len(failures) > 0:
			report.add_issue(
				Problem(
					'infrastructure-max-allowed-packet', Problem.ERROR,
					(
						messages.MAX_ALLOWED_MYSQL_PACKET_ISSUE_INTRO +
						"\n".join([
							messages.MAX_ALLOWED_MYSQL_PACKET_ISSUE_DESCRIPTION.format(
								source=_format_bytes(source_db_max_allowed_packet),
								target=_format_bytes(target_db_max_allowed_packet),
								source_host=source_host,
								target_host=target_host
							)
							for source_db_max_allowed_packet, target_db_max_allowed_packet, source_host, target_host in failures
						]) +
						"\n" + messages.SOLUTION_INCREASE_MAX_ALLOWED_PACKET + "\n"
					)
				), u""
			)
		else:
			report.add_issue(
				Problem(
					'infrastructure-max-allowed-packet', Problem.INFO,
					(
						messages.MAX_ALLOWED_PACKET_OK.format(
							description_target_database=description_target_database
						)
					)
				), u""
			)

	@staticmethod
	def _get_mysql_max_allow_packet_query():
		return u"SHOW VARIABLES LIKE 'max_allowed_packet'"

	@classmethod
	def _get_source_mysql_max_allowed_packet(cls, source_database_server):
		"""
		:type source_database_server: parallels.core.connections.database_server.DatabaseServer
		"""
		raise NotImplementedError()

	@classmethod
	def _get_target_mysql_max_allowed_packet(cls, target_database_server):
		"""
		:type target_database_server: parallels.core.connections.database_server.DatabaseServer
		"""
		raise NotImplementedError()


class UnixMysqlMaxAllowedPacketChecker(MysqlMaxAllowedPacketChecker):
	@classmethod
	def _get_source_mysql_max_allowed_packet(cls, source_database_server):
		"""
		:type source_database_server: parallels.core.connections.database_server.DatabaseServer
		"""
		with source_database_server.runner() as runner_source:
			return cls._mysql_max_allowed_packet_unix(
				runner_source, source_database_server.host(), source_database_server.port(),
				source_database_server.user(), source_database_server.password()
			)

	@classmethod
	def _get_target_mysql_max_allowed_packet(cls, target_database_server):
		"""
		:type target_database_server: parallels.core.connections.database_server.DatabaseServer
		"""
		with target_database_server.runner() as runner_target:
			return cls._mysql_max_allowed_packet_unix(
				runner_target, target_database_server.host(), target_database_server.port(),
				target_database_server.user(), target_database_server.password()
			)

	@classmethod
	def _mysql_max_allowed_packet_unix(cls, runner, host, port, login, password):
		# workaround for Plesk feature - it does not tell default MySQL server's admin password
		if host == 'localhost' and port == 3306:
			password_param = u" -p\"`cat /etc/psa/.psa.shadow`\""
		else:
			password_param = u" -p{password}"

		command = u"mysql -h {host} -P {port} -u{login} " + password_param + " --silent --skip-column-names -e {query}"
		args = dict(host=host, port=port, login=login, password=password, query=cls._get_mysql_max_allow_packet_query())

		return int(runner.sh(command, args)[len("max_allowed_packet"):])


class WindowsMysqlMaxAllowedPacketChecker(MysqlMaxAllowedPacketChecker):
	@classmethod
	def _get_source_mysql_max_allowed_packet(cls, source_database_server):
		"""
		:type source_database_server: parallels.core.connections.database_server.DatabaseServer
		"""
		source_database_server.panel_server.get_path_to_mysqldump()
		with source_database_server.panel_server.runner() as runner_source:
			return cls._mysql_max_allowed_packet_windows(
				runner_source, source_database_server.panel_server.get_path_to_mysql(),
				source_database_server.host(), source_database_server.port(), source_database_server.user(),
				source_database_server.password(),
				mysql_use_skip_secure_auth=source_database_server.panel_server.mysql_use_skip_secure_auth()
			)

	@classmethod
	def _get_target_mysql_max_allowed_packet(cls, target_database_server):
		"""
		:type target_database_server: parallels.core.connections.database_server.DatabaseServer
		"""
		target_mysql_bin=database_utils.get_windows_mysql_client(target_database_server.panel_server)
		with target_database_server.panel_server.runner() as runner_target:
			return cls._mysql_max_allowed_packet_windows(
				runner_target, target_mysql_bin, target_database_server.host(), target_database_server.port(),
				target_database_server.user(), target_database_server.password()
			)

	@classmethod
	def _mysql_max_allowed_packet_windows(
		cls, runner, mysql_bin, host, port, login, password, mysql_use_skip_secure_auth=False
	):
		mysql_output = cls._execute_windows_mysql_one_value_query(
			runner, mysql_bin, host, port, login, password, cls._get_mysql_max_allow_packet_query(),
			mysql_use_skip_secure_auth
		).strip()

		if mysql_output == 'NULL':
			return 0
		else:
			return int(mysql_output[len("max_allowed_packet"):])

	@staticmethod
	def _execute_windows_mysql_one_value_query(
		runner, mysql_bin, host, port, login, password, query, mysql_use_skip_secure_auth=False
	):
		command = (
			ur'cmd.exe /C "{mysql_bin} -h {host} -P {port} -u{login} -p{password}'
			ur'%s --silent --skip-column-names -e {query}"' % (
				' --skip-secure-auth' if mysql_use_skip_secure_auth else ''
			)
		)
		args = dict(mysql_bin=mysql_bin, host=host, port=port, login=login, password=password, query=query)
		exit_code, stdout, stderr = runner.sh_unchecked(command, args)
		if exit_code != 0:
			raise Exception(
				messages.MYSQL_COMMAND_NON_ZERO_EXIT_CODE, command, args,
				safe_string_repr(stdout), safe_string_repr(stderr)
			)
		return stdout

MSSQLConnectionData = namedtuple('MSSQLConnectionData', ('host', 'login', 'password'))


class MSSQLConnectionChecker:
	def check(self, nodes, mssql_connection_data, local_script_filepath, script_filename):
		target_script_temp_filepath = nodes.target.get_session_file_path(script_filename)
		logger.info(self._format_check_log_message(nodes, mssql_connection_data))

		with open(local_script_filepath, 'r') as f:
			with nodes.target.runner() as runner_target:
				runner_target.upload_file_content(target_script_temp_filepath, f.read())

		with nodes.target.runner() as runner_target:
			args = {
				'hostname': mssql_connection_data.host,
				'login': mssql_connection_data.login,
				'password': mssql_connection_data.password
			}
			exit_code, stdout, stderr = Powershell(runner_target, input_format_none=True).execute_script_unchecked(
				target_script_temp_filepath, args
			)
			if exit_code != 0 or stderr != '':
				cmd = Powershell(runner_target, input_format_none=True).get_command_string(
					target_script_temp_filepath, args
				)
				return Result.failure(self._format_script_failure(
					nodes, mssql_connection_data, cmd, exit_code, stdout, stderr
				))
			else:
				if stdout == 'OK\n':
					return Result.success(self._format_success(nodes, mssql_connection_data))
				else:
					return Result.failure(self._format_connection_failure(nodes, mssql_connection_data, stdout))

	@staticmethod
	def _format_success(nodes, mssql_connection_data):
		return messages.SUCCESSFULLY_CONNECTED_FROM_TARGET_MSSQL_SERVER.format(
			mssql_host=mssql_connection_data.host, mssql_user=mssql_connection_data.login,
			target=nodes.target.description()
		)

	@staticmethod
	def _format_connection_failure(nodes, mssql_connection_data, connection_error_message):
		return messages.UNABLE_TO_CONNECT_FROM_TARGET_MSSQL_SERVER.format(
			mssql_host=mssql_connection_data.host, mssql_user=mssql_connection_data.login,
			target=nodes.target.description(),
			connection_error_message=connection_error_message
		)

	@staticmethod
	def _format_script_failure(nodes, mssql_connection_data, command_str, exit_code, stdout, stderr):
		return messages.UNABLE_TO_CHECK_CONNECTION_FROM_TARGET_MSSQL.format(
			mssql_host=mssql_connection_data.host, mssql_user=mssql_connection_data.login,
			target=nodes.target.description(),
			command_str=command_str, exit_code=exit_code, stdout=stdout, stderr=stderr
		)

	@staticmethod
	def _format_check_log_message(nodes, mssql_connection_data):
		return messages.CHECK_CONNECTION_FROM_TARGET_MSSQL_SERVER.format(
			mssql_host=mssql_connection_data.host, mssql_user=mssql_connection_data.login,
			target=nodes.target.description()
		)


class PortChecker(object):
	def check(self, node):
		logger.info(self._format_log_message(node))

		ERRNO_CONNECTION_REFUSED = 111
		ERRNO_CONNECTION_TIMED_OUT = 110

		try:
			conn = socket.create_connection((node.ip(), self._get_port_number(node)))
			conn.close()
		except socket.error as e:
			if e.errno in (ERRNO_CONNECTION_REFUSED, ERRNO_CONNECTION_TIMED_OUT):
				return Result.failure(self._format_failure(node))
			else:
				raise

		return Result.success(self._format_success(node))

	@staticmethod
	def _get_port_number(node):
		raise NotImplementedError()

	def _format_failure(self, node):
		raise NotImplementedError()

	def _format_success(self, node):
		raise NotImplementedError()

	def _format_log_message(self, node):
		raise NotImplementedError()


class SourceNodeImapChecker(PortChecker):
	@staticmethod
	def _get_port_number(node):
		if node.mail_imap_encryption() == MailImapEncryption.SSL:
			return 993
		else:  # no encryption and TLS
			return 143

	def _format_log_message(self, node):
		return messages.LOG_CHECK_CONNECTION_BY_IMAP.format(
			node=node.description(),
			port_number=self._get_port_number(node)
		)

	def _format_failure(self, node):
		return messages.UNABLE_CONNECT_BY_IMAP_PORT.format(
			node=node.description(),
			port_number=self._get_port_number(node)
		)

	def _format_success(self, node):
		return messages.IMAP_CONNECTION_OK.format(
			node=node.description(),
			port_number=self._get_port_number(node)
		)


class SourceNodePop3Checker(PortChecker):
	@staticmethod
	def _get_port_number(node):
		return 110

	def _format_log_message(self, node):
		return messages.LOG_CONNECTION_BY_POP3.format(
			node=node.description(), 
			port_number=self._get_port_number(node)
		)

	def _format_failure(self, node):
		return messages.UNABLE_CONNECT_NODE_BY_POP3_PORT.format(
			node=node.description(), 
			port_number=self._get_port_number(node)
		)

	def _format_success(self, node):
		return messages.POP3_CONNECTION_OK.format(
			node=node.description(),
			port_number=self._get_port_number(node)
		)


class TargetNodeImapChecker(PortChecker):
	@staticmethod
	def _get_port_number(node):
		return 143

	def _format_log_message(self, node):
		return messages.LOG_CHECK_CONNECTION_BY_IMAP.format(
			node=node.description(), 
			port_number=self._get_port_number(node)
		)

	def _format_failure(self, node):
		return (
			messages.UNABLE_TO_CONNECT_TARGET_BY_IMAP_PORT).format(
			node=node.description(), 
			port_number=self._get_port_number(node)
		)

	def _format_success(self, node):
		return messages.IMAP_CONNECTION_OK.format(
			node=node.description(), 
			port_number=self._get_port_number(node)
		)


class CopySampleFileMessageFormatter(object):
	def __init__(self, nodes):
		self.nodes = nodes

	def format_failure(self, command_name, problem_description, failed_command_description):
		return messages.COPY_SAMPLE_FILE_FAILURE.format(
			command_name=command_name,
			source=self.nodes.source.description(),
			target=self.nodes.target.description(),
			problem_description=problem_description,
			failed_command_description=failed_command_description
		)

	def format_success(self, command_name):
		return messages.COPY_SAMPLE_FILE_OK.format(
			command_name=command_name,
			source=self.nodes.source.description(),
			target=self.nodes.target.description(),
		)

	def format_command_description(self, command_str, exit_code, stdout, stderr):
		return messages.COMMAND_DESCRIPTION.format(
			target=self.nodes.target.description(),
			command_str=command_str, exit_code=exit_code, stdout=stdout, stderr=stderr
		)

	def format_windows_rsync_problem_description(self):
		return messages.WINDOWS_RSYNC_PROBLEM_SOLUTION

	def format_check_log_message(self, command_name):
		return messages.LOG_COPY_SAMPLE_FILE.format(
			command_name=command_name,
			source=self.nodes.source.description(),
			target=self.nodes.target.description()
		)


class Result(object):
	@staticmethod
	def failure(msg):
		return False, msg

	@staticmethod
	def success(msg):
		return True, msg


@contextmanager
def temporary_file(filename, contents):
	try:
		with open(filename, 'w') as f:
			f.write(contents)
		yield
	finally:
		os.remove(filename)


class DiskSpaceDatabaseAccess(Entity):
	def __init__(self, host, port, user, password):
		self._host = host
		self._port = port
		self._user = user
		self._password = password

	@property
	def host(self):
		"""
		:rtype:
		"""
		return self._host

	@property
	def port(self):
		"""
		:rtype:
		"""
		return self._port

	@property
	def user(self):
		"""
		:rtype:
		"""
		return self._user

	@property
	def password(self):
		"""
		:rtype:
		"""
		return self._password


class DiskSpaceDatabase(Entity):
	def __init__(self, src_access, db_target_node, db_name):
		self._src_access = src_access
		self._db_target_node = db_target_node
		self._db_name = db_name

	@property
	def src_access(self):
		"""
		DiskSpaceDatabaseAccess (MSSQLConnectionData in case of MSSQL) for source server
		"""
		return self._src_access

	@property
	def db_target_node(self):
		"""
		:rtype: parallels.core.connections.database_server.DatabaseServer
		"""
		return self._db_target_node

	@property
	def db_name(self):
		"""
		:rtype:
		"""
		return self._db_name


class UnixDiskSpaceSourcePleskNode(Entity):
	def __init__(self, node, web_domains, mail_domains, mysql_databases):
		self._node = node
		self._web_domains = web_domains
		self._mail_domains = mail_domains
		self._mysql_databases = mysql_databases

	@property
	def node(self):
		"""
		:rtype:
		"""
		return self._node

	@property
	def web_domains(self):
		"""
		:rtype:
		"""
		return self._web_domains

	@property
	def mail_domains(self):
		"""
		:rtype:
		"""
		return self._mail_domains

	@property
	def mysql_databases(self):
		"""
		:rtype:
		"""
		return self._mysql_databases


class UnixDiskSpaceChecker(object):
	def check(self, target_node, source_nodes, report):
		"""Check source and target server disk space requirements.
		
		Calculate disk usage on source server for web hosting, mail, database.
		Run target server disk space usage calculation function, which updates
		the Report object.

		Args:
			target_node:
				target_panel_plesk.connections.target_server.PleskTargetServer
			source_nodes:
				a structure, that maps source servers to domains, which hosted
				on that servers
			report:
				report storage object parallels.core.checking.Report

		:rtype: None
		"""
		usage_source_web = 0
		usage_source_mail = 0
		usage_source_mysql_db = 0
		all_usage_source_mysql_db = [0]
		for source_node in source_nodes:
			usage_source_web += self._get_source_unix_domains_disk_usage_web(
				source_node.node, source_node.web_domains
			)
			usage_source_mail += self._get_source_unix_domains_disk_usage_mail(
				source_node.node, source_node.mail_domains
			)

			with source_node.node.runner() as runner_source:
				for db in source_node.mysql_databases:
					source_db_server = db.source_database_server
					single_usage_source_mysql_db = _estimate_mysql_database_size(
						runner_source, source_db_server.host(), source_db_server.port(),
						source_db_server.user(), source_db_server.password(), db.database_name
					)
					usage_source_mysql_db += single_usage_source_mysql_db
					all_usage_source_mysql_db.append(single_usage_source_mysql_db)

		logger.debug(
			messages.LOG_TOTAL_DISK_USAGE_ON_SOURCE,
			target_node.description(), usage_source_web, usage_source_mail, usage_source_mysql_db
		)

		self.check_with_source_usages(
			target_node, report,
			usage_source_web, usage_source_mail, usage_source_mysql_db, max(all_usage_source_mysql_db),
			itertools.chain(*[source_node.web_domains for source_node in source_nodes]),
			itertools.chain(*[source_node.mail_domains for source_node in source_nodes]),
			itertools.chain(*[source_node.mysql_databases for source_node in source_nodes]),
		)

	def check_with_source_usages(
		self, target_node, report, usage_source_web, usage_source_mail,
		usage_source_mysql_db, max_usage_source_mysql_db, web_domains,
		mail_domains, mysql_databases
	):
		"""Check, if there is enough disk space on target server.
		
		Args:
			target_node:
				target server
			report: report Object to be updated
			usage_source_web, usage_source_mail,
			usage_source_mysql_db, max_usage_source_mysql_db:
				disk space consumed on source server by each service
			web_domains, mail_domains:
				list of domains, that use the service type
			mysql_databases:
				list of databases to be migrated
			
		Returns: None. The result is the new records added to the report
		object.
		"""
		hosting_server = target_node.get_hosting_server()
		usage_target_web = self._get_unix_domains_disk_usage(
			target_node, web_domains, hosting_server.get_unix_vhost_dirs
		)
		usage_target_mail = self._get_unix_domains_disk_usage(
			target_node, mail_domains,
			hosting_server.get_unix_domain_mail_dirs
		)
		usage_target_mysql_db = self._get_target_mysql_databases_usage(
			target_node, mysql_databases
		)

		logger.debug(
			messages.LOG_TOTAL_DISK_USAGE_ON_TARGET,
			target_node.description(), usage_target_web, usage_target_mail, usage_target_mysql_db
		)

		with target_node.runner() as runner:
			required = defaultdict(int)
			available = defaultdict(int)

			web_dir = target_node.vhosts_dir 
			if self._file_exists(runner, web_dir):
				web_device, web_available = _get_available_disk_space(runner, web_dir)
				required[web_device] += _positive(usage_source_web - usage_target_web)
				available[web_device] = web_available

			mail_dir = target_node.mail_dir 
			if self._file_exists(runner, mail_dir):
				mail_device, mail_available = _get_available_disk_space(runner, mail_dir)
				required[mail_device] += _positive(usage_source_mail - usage_target_mail)
				available[mail_device] = mail_available

			mysql_data_dir = self._get_mysql_data_dir(runner)
			if self._file_exists(runner, mysql_data_dir):
				mysql_device, mysql_available = _get_available_disk_space(runner, mysql_data_dir)
				required[mysql_device] += _positive(usage_source_mysql_db - usage_target_mysql_db)
				available[mysql_device] = mysql_available

			if self._file_exists(runner, target_node.session_dir()):
				session_device, session_available = _get_available_disk_space(runner, target_node.session_dir())
				# Check that there is enough space for temporary database dump file in migrator session directory.
				# Also note that database dump could take much less or much more than estimated, it depends on database data. 
				# But we just require this space to avoid disk space issues.
				required[session_device] += max_usage_source_mysql_db
				available[session_device] = session_available

		failures = []
		for device in required.keys():
			logger.debug(messages.LOG_REQUIRED_DISK_SPACE, device, required[device])
			logger.debug(messages.LOG_AVAILABLE_DISK_SPACE, device, available[device])
			if required[device] > available[device]:
				failures.append((device, required[device], available[device]))

		if len(failures) > 0:
			report.add_issue(
				Problem(
					'infrastructure-disk-space', Problem.ERROR, 
					(
						messages.INSUFFICIENT_DISK_SPACE_TARGET_.format(
							target=target_node.description()
						) +
						"\n".join([
							messages.DISK_SPACE_LACK.format(
								device=device, required=_format_bytes(required_device),
								available=_format_bytes(available_device)
							)
							for device, required_device, available_device in failures
						]) +
						messages.INSUFFICIENT_DISK_SPACE_SOLUTION)
				), u""
			)
		else:
			report.add_issue(
				Problem(
					'infrastructure-disk-space', Problem.INFO, 
					messages.ENOUGH_DISK_SPACE_FOR_CONTENT.format(target=target_node.description())
				), u""
			)

	@staticmethod
	def _file_exists(runner, filepath):
		exit_code, _, _ = runner.run_unchecked('/usr/bin/test', ['-e', filepath])
		return exit_code == 0

	@classmethod
	def _get_source_unix_domains_disk_usage_web(cls, server, domains):
		diskusage_function = server.get_unix_vhost_dirs
		return cls._get_unix_domains_disk_usage(
			server, domains, diskusage_function
		)

	@classmethod
	def _get_source_unix_domains_disk_usage_mail(cls, server, domains):
		diskusage_function = server.get_unix_domain_mail_dirs
		return cls._get_unix_domains_disk_usage(
			server, domains, diskusage_function
		)

	@classmethod
	def _get_unix_domains_disk_usage(cls, server, domains, diskusage_function):
		def domain_disk_usage(domain, runner):
			directory_list = diskusage_function(runner, domain)
			diskspace = 0
			for dir in directory_list:
				if cls._file_exists(runner, dir):
					diskspace += cls._get_used_disk_space(runner, dir)
			return diskspace

		with server.runner() as runner:
			return sum([domain_disk_usage(domain, runner) for domain in domains])

	@staticmethod
	def _get_used_disk_space(runner, filepath):
		# Example output of 'du':
		# 1572864 /var/www/vhosts/example.com/
		#
		# We need just to take the first number
		return int(
			runner.run('/usr/bin/du', ['-s', '--block-size=1', filepath]).split()[0]
		)
	
	@staticmethod
	def _get_target_mysql_databases_usage(target_node, databases):
		"""
		:type databases: list[parallels.core.utils.database_utils.DatabaseInfo]
		"""
		with target_node.runner() as runner:
			return sum([
				_estimate_mysql_database_size(
					runner, db.target_database_server.host(), db.target_database_server.port(),
					db.target_database_server.user(), db.target_database_server.password(),
					db.database_name
				)
				for db in databases
			])

	@classmethod
	def _get_mysql_data_dir(cls, runner):
		mycnf_filepath = '/etc/my.cnf'
		default_mysql_data_dir =  '/var/lib/mysql'
		if cls._file_exists(runner, mycnf_filepath):
			for line in runner.get_file_contents(mycnf_filepath).split(): 
				# simply look for the first "datadir=" occurrence
				# it won't work correctly if there are multiple sections with different datadir, 
				# however as probability of such situation is low, we don't consider that case
				# also comments on the same line as datadir parameter are not supported
				m = re.match('^datadir\s*=\s*(.*)$', line.strip())
				if m is not None:
					return m.group(1)

			return default_mysql_data_dir # datadir parameter not found
		else:
			return default_mysql_data_dir


class UnixSourceSessionDirDiskSpaceChecker(object):
	"""Check that source server has enough disk space for temporary files"""

	@staticmethod
	def check(source_server, mysql_databases, report):
		"""
		:type mysql_databases: list[parallels.core.utils.database_utils.DatabaseInfo]
		:type report: parallels.core.checking.Report
		"""
		all_usage_source_mysql_db = [0]
		with source_server.runner() as runner:
			for db in mysql_databases:
				source_db_server = db.source_database_server
				all_usage_source_mysql_db.append(
					_estimate_mysql_database_size(
						runner, source_db_server.host(), source_db_server.port(),
						source_db_server.user(), source_db_server.password(), db.database_name
					)
				)
			max_usage = max(all_usage_source_mysql_db)
			session_device, session_available = _get_available_disk_space(runner, source_server.session_dir())
		if session_available < max_usage:
			report.add_issue(
				Problem(
					'infrastructure-disk-space', Problem.ERROR, 
					(
						messages.SESSION_DIR_INSUFFICIENT_DISK_SPACE_SOURCE_FAILURE.format(
							source=source_server.description()
						) +
						messages.DISK_SPACE_LACK.format(
							device=session_device, required=_format_bytes(max_usage), available=_format_bytes(session_available)
						) + 
						messages.SESSION_DIR_INSUFFICIENT_DISK_SPACE_SOURCE_SOLUTION)
				), u""
			)
		else:
			report.add_issue(
				Problem(
					'infrastructure-disk-space', Problem.INFO, 
					(
						messages.SESSION_DIR_ENOUGH_DISK_SPACE_SOURCE.format(
							source=source_server.description().capitalize()
						)
					)
				), u""
			)

WindowsDiskSpaceSourceNode = namedtuple('WindowsDiskSpaceSourcePleskNode', (
	'node', 'domains', 'mysql_databases', 'mssql_databases'
))


class WindowsDiskSpaceChecker(object):
	def check(self, target_node, mysql_bin, source_nodes, report):

		usage_source_web = 0
		# { db_server: [usage, ...] }
		usages_source_mysql_db = defaultdict(list)
		# { db_server: [usage, ...] }
		usages_source_mssql_db = defaultdict(list)
		target_mysql_databases = []
		target_mssql_databases = []

		mssql_disk_usage_script_target_path = target_node.get_session_file_path(self.mssql_disk_usage_script)
		with target_node.runner() as runner_target:
			runner_target.upload_file(self.mssql_disk_usage_script_local_path, mssql_disk_usage_script_target_path)
		for source_node in source_nodes:
			for db in source_node.mssql_databases:
				_, usage_source_mssql_db = _estimate_mssql_database_size(
					mssql_disk_usage_script_target_path,
					runner_target, db.src_access.host, db.src_access.login,
					db.src_access.password, db.db_name)
				usages_source_mssql_db[db.db_target_node.host()].append(usage_source_mssql_db)
				target_mssql_databases.append(
					obj(
						target_node=target_node, db_name=db.db_name,
						db_target_node=db.db_target_node))

		for source_node in source_nodes:
			du_script_path_source_node = source_node.node.get_session_file_path(self.disk_usage_script_name)
			with source_node.node.runner() as runner_source:
				runner_source.upload_file(self.disk_usage_script_local_path, du_script_path_source_node)
				usage_source_web += self._get_windows_domains_disk_usage_web(
					source_node.node, source_node.domains, du_script_path_source_node
				)
				runner_source.remove_file(du_script_path_source_node)

				for db in source_node.mysql_databases:
					usage_source_mysql_db = _estimate_mysql_database_size_windows_on_source_plesk(
						source_node.node, db.src_access.host, db.src_access.port,
						db.src_access.user, db.src_access.password, db.db_name
					)
					usages_source_mysql_db[(db.db_target_node.host(), db.db_target_node.port())].append(
						usage_source_mysql_db
					)
					target_mysql_databases.append(
						obj(
							target_node=target_node,
							db_name=db.db_name,
							db_target_node=db.db_target_node
						)
					)

		self.check_with_source_usages(
			target_node, mysql_bin,
			usage_source_web, usages_source_mysql_db, usages_source_mssql_db,
			itertools.chain(*[source_node.domains for source_node in source_nodes]),
			target_mysql_databases,
			target_mssql_databases,
			report
		)

	def check_with_source_usages(
		self, target_node, mysql_bin, usage_source_web, usages_source_mysql_db, usages_source_mssql_db,
		domains, mysql_databases, mssql_databases, report
	):
		required = defaultdict(int)
		available = defaultdict(int)

		du_script_path_target_node = target_node.get_session_file_path(self.disk_usage_script_name)
		mssql_disk_usage_script_target_path = target_node.get_session_file_path(self.mssql_disk_usage_script)
		with target_node.runner() as runner_target:
			runner_target.upload_file(self.disk_usage_script_local_path, du_script_path_target_node)
			runner_target.upload_file(self.mssql_disk_usage_script_local_path, mssql_disk_usage_script_target_path)

			usage_target_web = self._get_windows_domains_disk_usage_web(
				target_node, domains, du_script_path_target_node
			)

			# { db_server: usage }
			usages_target_mysql_db = defaultdict(int)
			# { db_server: disk_letter }
			disk_letters_mysql_db = {}
			# { db_server: usage }
			usages_target_mssql_db = defaultdict(int)
			# { db_server: disk_letter }
			disk_letters_mssql_db = {}
			for db in mysql_databases:
				usage_target_mysql_db = _estimate_mysql_database_size_windows_on_target_node(
					runner_target, mysql_bin, db.db_target_node.host(), db.db_target_node.port(),
					db.db_target_node.user(), db.db_target_node.password(), db.db_name
				)
				usages_target_mysql_db[(db.db_target_node.host(), db.db_target_node.port())] += usage_target_mysql_db

				db_disk_letter = _get_windows_disk_letter(
					_get_windows_mysql_datadir_on_target_node(
						runner_target, mysql_bin, db.db_target_node.host(), db.db_target_node.port(),
						db.db_target_node.user(), db.db_target_node.password()
					)
				)
				disk_letters_mysql_db[(db.db_target_node.host(), db.db_target_node.port())] = db_disk_letter

			for db in mssql_databases:
				db_disk_letter, usage_target_mssql_db = _estimate_mssql_database_size(
					mssql_disk_usage_script_target_path,
					runner_target, db.db_target_node.host(), db.db_target_node.user(),
					db.db_target_node.password(), db.db_name
				)

				usages_target_mssql_db[db.db_target_node.host()] += usage_target_mssql_db
				disk_letters_mssql_db[db.db_target_node.host()] = db_disk_letter

			for mysql_db_server in usages_source_mysql_db:
				required[disk_letters_mysql_db[mysql_db_server]] += (
					_positive(sum(usages_source_mysql_db[mysql_db_server]) - usages_target_mysql_db[mysql_db_server])
				)
			for mssql_db_server in usages_source_mssql_db:
				required[disk_letters_mssql_db[mssql_db_server]] += (
					_positive(sum(usages_source_mssql_db[mssql_db_server]) - usages_target_mssql_db[mssql_db_server])
				)

			session_disk_letter = _get_windows_disk_letter(target_node.session_dir())
			if len(usages_source_mysql_db) > 0:
				required[session_disk_letter] += max(itertools.chain(*usages_source_mysql_db.values()))

			# Starting from Plesk 11.5, dbbackup.exe puts database backup into
			# %PLESK_DIR%/PrivateTemp on the target node, then restores the backup.
			# We require disk space equal to maximum source database size for the disk where %PLESK_DIR% is located.
			plesk_dir_disk_letter = _get_windows_disk_letter(target_node.plesk_dir)
			if len(usages_source_mssql_db) > 0:
				required[plesk_dir_disk_letter] += max(itertools.chain(*usages_source_mssql_db.values()))

			runner_target.remove_file(du_script_path_target_node)
			web_content_disk_letter = _get_windows_disk_letter(target_node.vhosts_dir)
			required[web_content_disk_letter] += _positive(usage_source_web - usage_target_web)

			failures = []
			for disk_letter, required_for_disk in required.iteritems():
				available_for_disk = _get_windows_disk_free_space(runner_target, disk_letter)
				available[disk_letter] = available_for_disk
				logger.debug(messages.LOG_REQUIRED_DISK_SPACE_WINDOWS, disk_letter, required_for_disk)
				logger.debug(messages.LOG_AVAILABLE_DISK_SPACE_WINDOWS, disk_letter, available_for_disk)

				if required_for_disk > available_for_disk:
					failures.append((disk_letter, required_for_disk, available_for_disk))

			if len(failures) > 0:
				report.add_issue(
					Problem(
						'infrastructure-disk-space', Problem.ERROR, 
						(
							messages.INSUFFICIENT_DISK_SPACE_TARGET_WINDOWS_ISSUE.format(
								target=target_node.description()
							) +
							"\n".join([
								messages.EXPECTED_DISK_SPACE_BY_WINDOWS_DISKS.format(
									disk_letter=disk_letter, required=_format_bytes(required_for_disk),
									available=_format_bytes(fail_available_for_disk)
								)
								for disk_letter, required_for_disk, fail_available_for_disk in failures
							]) +
							messages.INSUFFICIENT_DISK_SPACE_TARGET_WINDOWS_SOLUTION)
					), u""
				)
			else:
				report.add_issue(
					Problem(
						'infrastructure-disk-space', Problem.INFO, 
						(
							messages.ENOUGH_DISK_SPACE_FOR_CONTENT.format(
								target=target_node.description()
							)
						)
					), u""
				)

	@property
	def disk_usage_script_name(self):
		return 'folder_disk_usage.vbs'

	@property
	def mssql_disk_usage_script(self):
		return 'mssql_disk_usage.ps1'

	@property
	def disk_usage_script_local_path(self):
		return migrator_utils.get_package_scripts_file_path(
			parallels.plesk.source.plesk, self.disk_usage_script_name
		)

	@property
	def mssql_disk_usage_script_local_path(self):
		return migrator_utils.get_package_scripts_file_path(
			parallels.plesk.source.plesk, self.mssql_disk_usage_script
		)

	@classmethod
	def _get_windows_domains_disk_usage_web(cls, server, domains, du_remote_script_path):
		def domain_disk_usage(domain):
			with server.runner() as runner:
				domain_directory = server.get_vhost_dir(domain)
				if windows_utils.file_exists(runner, domain_directory):
					return int(
						runner.sh(
							u"cmd.exe /C cscript {script_path} //NoLogo {domain_directory}", dict(
								script_path=du_remote_script_path, domain_directory=domain_directory)
						).strip()
						)
				else:
					return 0

		total_disk_usage = sum([domain_disk_usage(domain) for domain in domains])
		return total_disk_usage


class WindowsSourceSessionDirDiskSpaceChecker(object):
	"""Check that source server has enough disk space for temporary files"""

	@staticmethod
	def check(source_server, mysql_databases, report):
		"""
		:type mysql_databases: list[parallels.core.utils.database_utils.DatabaseInfo]
		:type report: parallels.core.checking.Report
		"""
		all_usage_source_mysql_db = [0]
		for db in mysql_databases:
			source_db_server = db.source_database_server
			usage_source_mysql_db = _estimate_mysql_database_size_windows_on_source_plesk(
				source_server, source_db_server.host(), source_db_server.port(),
				source_db_server.user(), source_db_server.password(), db.database_name
			)
			all_usage_source_mysql_db.append(usage_source_mysql_db)

		max_usage = max(all_usage_source_mysql_db)

		session_disk_letter = _get_windows_disk_letter(source_server.session_dir())
		with source_server.runner() as runner:
			session_available = _get_windows_disk_free_space(runner, session_disk_letter)

		logger.debug(
			messages.LOG_REQUIRED_DISK_SPACE_SOURCE, source_server.description(), session_disk_letter, max_usage
		)
		logger.debug(
			messages.LOG_AVAILABLE_DISK_SPACE_SOURCE, source_server.description(), session_disk_letter,
			session_available
		)

		if session_available < max_usage:
			report.add_issue(
				Problem(
					'infrastructure-disk-space', Problem.ERROR, 
					(
						messages.INSUFFICIENT_WINDOWS_DISK_SPACE_SOURCE_ISSUE.format(
							description=source_server.description()
						) +
						messages.REQUIRED_DISK_SPACE_BY_LETTER.format(
							disk_letter=session_disk_letter, required=_format_bytes(max_usage),
							available=_format_bytes(session_available)
						) + 
						messages.INSUFFICIENT_WINDOWS_DISK_SPACE_SOURCE_SOLUTION)
				), u""
			)
		else:
			report.add_issue(
				Problem(
					'infrastructure-disk-space', Problem.INFO, 
					(
						messages.ENOUGH_DISK_SPACE_WINDOWS_SOURCE.format(
							source=source_server.description().capitalize()
						)
					)
				), u""
			)


def check_database_server_connections(report, target_db_servers):
	"""
	:type target_db_servers: list[parallels.core.connections.database_server.DatabaseServer]
	"""
	logger.info(messages.LOG_CHECK_CONNECTIONS_TO_TARGET_DATABASE_SERVERS)
	db_report = report.subtarget(messages.REPORT_TITLE_CONNECTIONS_TO_TARGET_DATABASE_SERVERS, None)
	for server in target_db_servers:
		try:
			logger.debug(messages.LOG_CHECK_CONNECTION_TO_TARGET_DATABASE_SERVER, server.description())
			server.check_connection()
		except DatabaseServerConnectionException as e:
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)
			db_report.add_issue(
				checking.Problem(
					'target_database_server_connection_issue',
					checking.Problem.ERROR, str(e),
				),
				messages.DATABASE_SERVER_CONNECTION_ISSUE
			)
		except Exception as e:
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)
			db_report.add_issue(
				checking.Problem(
					'target_database_server_connection_internal_error',
					checking.Problem.WARNING,
					messages.INTERNAL_ERROR_CHECKING_TARGET_DATABASE_SERVER % (
						server.description(), str(e)
					),
				),
				messages.CHECK_SKIPPED
			)
		else:
			db_report.add_issue(
				checking.Problem(
					'target_database_server_connection_ok',
					checking.Problem.INFO,
					messages.SUCCESSFULLY_CONNECTED_TO % server.description(),
				), ""
			)


def _get_available_disk_space(runner, filepath):
	"""Get available disk space in bytes on filesystem where file or directory 'filepath' is located"""
	return _get_device_available_bytes_from_df_output(
		runner.run('/bin/df', [
			# in certain cases df outputs usage in a format difficult to parse
			# (device and usage are on different lines), we use "-P" to make its behavior consistent
			'-P',
			filepath
		])
	)


def _get_device_available_bytes_from_df_output(output):
	"""Parse output of 'df' Unix utility and return tuple (device, number available bytes)
	on the first filesystem of output"""

	lines = output.split("\n")
	if len(lines) < 2:
		raise Exception(messages.UNIX_DF_OUTPUT_PARSE_ISSUE_EXPECTED_MORE_LINES % (len(lines), output))

	# Example output:
	#
	# Filesystem         1024-blocks      Used Available Capacity Mounted on
	# /dev/hda1             27769928   1654556  24681976       7% /
	#
	# ^^^^^^^^^                                 ^^^^^^^^
	# we need the items above 

	fs_line_items = lines[1].split()
	if len(fs_line_items) < 4:
		raise Exception(messages.UNIX_DF_OUTPUT_PARSE_ISSUE_EXPECTED_AT_LEAST_ONE_ITEM % (fs_line_items,))

	return fs_line_items[0], int(fs_line_items[3]) * 1024


def _get_windows_disk_letter(path):
	disk_letter, _ = path.split(':')
	return disk_letter.upper() 


def _get_windows_disk_free_space(runner, disk_letter):
	# Sample output of fsutil utility:
	#
	# Total # of free bytes        : 31998562304
	# Total # of bytes             : 53684989952
	# Total # of avail free bytes  : 31998562304

	output = runner.sh(u"fsutil volume diskfree {disk_letter}:", dict(disk_letter=disk_letter))
	first_line = output.split("\n")[0]
	_, bytes_count_str = first_line.split(':')
	return int(bytes_count_str.strip())


def _estimate_mysql_database_size(runner, host, port, login, password, db_name):
	"""Please consider that this is estimation only, so:
	- On the target box database could take more or less space
	- Database text dump file could take *much* more or *much* less space - it depends on data.

	However we can use this estimation as some minimal requirement for migration.
	"""

	if (host, port, db_name) not in _estimate_mysql_database_size.cache:
		try:
			query = u"""
			SELECT
				COALESCE(SUM(data_length), 0)
			FROM information_schema.TABLES
			WHERE information_schema.TABLES.TABLE_SCHEMA = '{db_name}'
			""".format(db_name=db_name)

			# workaround for Plesk feature - it does not tell default MySQL server's admin password
			if host == 'localhost' and port == 3306:
				password_param = u" -p\"`cat /etc/psa/.psa.shadow`\""
			else:
				password_param = u" -p{password}"

			command = u"mysql -h {host} -P {port} -u{login} " + password_param + " --silent --skip-column-names -e {query}"
			args = dict(host=host, port=port, login=login, password=password, query=query)

			_estimate_mysql_database_size.cache[(host, port, db_name)] = int(
				runner.sh(command, args)
			)
		except Exception as err:
			# if there are connection problems - just consider database uses 0 bytes
			logger.debug(messages.LOG_EXCEPTION, exc_info=err)
			_estimate_mysql_database_size.cache[(host, port, db_name)] = 0

	return _estimate_mysql_database_size.cache[(host, port, db_name)]

_estimate_mysql_database_size.cache = dict()


def _estimate_mssql_database_size(dbsize_script_path, runner, host, login, password, db_name):
	try:
		def parse_disk_usage(s):
			sizes = { 
				'': 1,
				'KB': 1024,
				'MB': 1024 * 1024,
				'GB': 1024 * 1024 * 1024,
				'TB': 1024 * 1024 * 1024 * 1024,
			}
			m = re.match("(\d+(.\d+))\s*(KB|MB|GB|TB|)", s)
			if m is not None:
				return int(float(m.group(1)) * sizes[m.group(3)])
			else:
				raise Exception(messages.MSSQL_UNABLE_TO_PARSE_DISK_SPACE_SCRIPT_OUTPUT % (s,))

		# "-InputFormat none" is required for powershell not to hang
		# See http://stackoverflow.com/questions/4238192/running-powershell-from-msdeploy-runcommand-does-not-exit/
		cmd = (
			"powershell -InputFormat none -ExecutionPolicy Unrestricted"
			" -File {dbsize_script_path} -hostname {host} -database {database}"
			" -login {login} -password {password}"
		)
		args = dict(
			dbsize_script_path=dbsize_script_path,
			host=host, database=db_name, login=login, password=password
		)

		usage_info = {}
		error_code, stdout, stderr = runner.sh_unchecked(cmd, args)
		for line in stdout.split("\n"):
			line = line.strip()
			if line != '':
				name, value = line.split(" ", 1)
				usage_info[name] = value

		if 'database_size' not in usage_info or 'db_file_location' not in usage_info:
			raise Exception(
				messages.MSSQL_UNABLE_TO_FIND_DB_SIZE_IN_SCRIPT_OUTPUT % (
					windows_utils.format_command(cmd, **args), error_code, stdout, stderr)
			)

		return _get_windows_disk_letter(usage_info['db_file_location']), parse_disk_usage(usage_info['database_size'])
	except Exception as err:
		# for example, if database does not exist or there are connection
		# problems - just consider database uses 0 bytes not to break all checks
		logger.debug(messages.LOG_EXCEPTION, exc_info=err)
		return 'C', 0


def _estimate_mysql_database_size_windows_on_source_plesk(source_node, host, port, login, password, db_name):
	plesk_dir = source_node.plesk_dir
	mysql_bin = ur"{plesk_dir}\MySQL\bin\mysql".format(plesk_dir=plesk_dir)
	with source_node.runner() as runner:
		return _estimate_mysql_database_size_windows(
			runner, mysql_bin, host, port, login, password, db_name
		)


def _estimate_mysql_database_size_windows_on_target_node(runner, mysql_bin, host, port, login, password, db_name):
	return _estimate_mysql_database_size_windows(
		runner, mysql_bin, host, port, login, password, db_name
	)


def _estimate_mysql_database_size_windows(runner, mysql_bin, host, port, login, password, db_name):
	try:
		mysql_output = _execute_windows_mysql_one_value_query(
			runner, mysql_bin, host, port, login, password, db_name, query=_get_estimate_mysql_database_size_query(db_name)
		).strip()
		
		if mysql_output == 'NULL':
			return 0
		else:
			return int(mysql_output)
	except Exception as err:
		# for example, if database does not exist or there are connection
		# problems - just consider database uses 0 bytes not to break all checks
		logger.debug(messages.LOG_EXCEPTION, exc_info=err)
		logger.debug(messages.NOTICE_THAT_WE_IGNORE_EXCEPTION_AND_USE_0_AS_DISK_SPACE_USAGE)
		return 0


def _get_windows_mysql_datadir_on_target_node(runner, mysql_bin, host, port, login, password):
	query = u"SHOW VARIABLES WHERE Variable_name = 'datadir'"
	command = (
		ur'cmd.exe /C "{mysql_bin} -h {host} -P {port} -u{login} -p{password} '
		ur'--silent --skip-column-names -e {query}"'
	)
	args = dict(mysql_bin=mysql_bin, host=host, port=port, login=login, password=password, query=query)
	return runner.sh(command, args).strip().split()[1]


def _execute_windows_mysql_one_value_query(runner, mysql_bin, host, port, login, password, db_name, query):
	command = (
		ur'cmd.exe /C "{mysql_bin} -h {host} -P {port} -u{login} -p{password} {db_name} '
		ur'--silent --skip-column-names -e {query}"'
	)
	args = dict(mysql_bin=mysql_bin, host=host, port=port, login=login, password=password, db_name=db_name, query=query)
	exit_code, stdout, stderr = runner.sh_unchecked(command, args)
	if exit_code != 0:
		raise Exception(
			messages.MYSQL_COMMAND_NON_ZERO_EXIT_CODE % (
				command, args, safe_string_repr(stdout), safe_string_repr(stderr)
			)
		)
	return stdout


def _get_estimate_mysql_database_size_query(db_name):
	return messages.SELECT_SUMDATA_LENGTH_FROM_INFORMATION_SCHEMATABLES.format(db_name=db_name)


def _format_bytes(bytes_count):
	KB = 1024.0
	MB = 1024.0 * KB
	GB = 1024.0 * MB

	if bytes_count < KB:
		return u"%s bytes" % (bytes_count,)
	elif bytes_count < MB:
		return u"%.2fKB" % (bytes_count / KB)
	elif bytes_count < GB:
		return u"%.2fMB" % (bytes_count / MB)
	else:
		return u"%.2fGB" % (bytes_count / GB)


def _positive(i):
	return i if i > 0 else 0


def check_windows_copy_content_rsync_connection(subscriptions_info, get_rsync, content_type, report):
	"""
	Arguments:
	- subscriptions_info - list of subscriptions with info about nodes,
	instance of SubscriptionsInfo
	- get_rsync - function to retrive rsync
	- content_type -  type of content for which testing connection between
	nodes
	- report - storage object parallels.core.checking.Report
	"""
	NodesPair = namedtuple('NodesPair', ('target', 'source_ip'))
	# form up the list of subscriptions which will be affected
	# for each pair of <target, source_ip>, try retrieving the list of web files of any one related subscription
	subscriptions_by_nodes_pair = group_by(
		subscriptions_info, lambda si: NodesPair(target=si.target_node, source_ip=si.source_ip)
	)

	for nodes_info, subscriptions in subscriptions_by_nodes_pair.iteritems():
		try:
			rsync = get_rsync(None, nodes_info.target, nodes_info.source_ip)
			if content_type == 'web':
				rsync.list_files('vhosts/%s/' % subscriptions[0].subscription_dir)
			elif content_type == 'mail':
				rsync.list_files('migrator/')
		except RsyncClientNonZeroExitCode as e:
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)

			report.add_issue(
				checking.Problem(
					'infrastructure-web', checking.Problem.ERROR,
					messages.RSYNC_CONNECTION_WINDOWS_COPY_CONTENT_FAILURE % (
						nodes_info.target.description(),
						nodes_info.source_ip,
						safe_string_repr("\n".join(e.stderr.strip().split("\n")[-5:])),
						content_type,
						format_list([s.subscription_name for s in subscriptions])
					)
				),
				messages.RSYNC_CONNECTION_WINDOWS_COPY_CONTENT_SOLUTION.format(cmd=e.command))
			return

		report.add_issue(
			checking.Problem(
				'infrastructure-web', checking.Problem.INFO,
				messages.RSYNC_CONNECTION_WINDOWS_COPY_CONTENT_SUCCESS % (
					nodes_info.target.description(),
					nodes_info.source_ip
				)
			),
			u""
		)
