from parallels.source.plesk import messages
import os
import re
import logging
from collections import namedtuple, defaultdict
from contextlib import contextmanager
import socket
import itertools

from parallels.core.utils import unix_utils, migrator_utils, windows_utils, database_utils
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

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, 
						("%s\nFix found issues and run the transfer-accounts command again.\n" + 
						"If the issues are not fixed, it will be not possible to transfer databases for the following subscription(s): %s"
						) % (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, 
						("%s\nFix found issues and run the transfer-accounts command again.\n" + 
						"If the issues are not fixed, it will be not possible to transfer %s for the following subscription(s): %s"
						) % (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("Target main node node disk space check - subscriptions count: %s, required: %r, available: %r", 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 WindowsFileCopyBetweenNodesRsyncChecker(object):
	def check(self, nodes, filename, local_temp_filepath, get_rsync):
		"""
		Arguments:
			nodes - instance of NodesPair class
			source_temp_filename - name of temporary file in migration session directory of the source server
			source_temp_filepath - full path to a temporary file in migration session directory of the source server
		Returns tuple with:
			1st element: True if check passed successfully, False otherwise
			2nd element: None if check passed successfully, string describing failure reason otherwisersync_binary_path - path of rsync.exe on the target node
		"""
		message_formatter = CopySampleFileMessageFormatter(nodes)
		logger.info(message_formatter.format_check_log_message(self.get_command_name()))

		target_temp_filepath = nodes.target.get_session_file_path(filename)
		source_temp_filepath = nodes.source.get_session_file_path(filename)

		with nodes.source.runner() as runner_source:
			with temporary_file(local_temp_filepath, contents='test-file-contents'):
				runner_source.upload_file(local_temp_filepath, source_temp_filepath)

		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(object):
	def check(self, nodes, filename, local_temp_filepath, ssh_key_pool):
		"""
		:type ssh_key_pool: parallels.core.utils.ssh_key_pool.SSHKeyPool

		Arguments:
			nodes - instance of NodesPair class
			source_user - check connection to source server under this user
		Returns tuple with:
			1st element: True if check passed successfully, False otherwise
			2nd element: None if check passed successfully, string describing failure reason otherwisersync_binary_path - path of rsync.exe on the target node
		"""
		message_formatter = CopySampleFileMessageFormatter(nodes)
		logger.info(message_formatter.format_check_log_message(self.get_command_name()))

		source_temp_filepath = nodes.source.get_session_file_path(filename)
		target_temp_filepath = nodes.target.get_session_file_path(filename)

		test_file_contents = 'test-file-contents'
		with temporary_file(local_temp_filepath, test_file_contents):
			with nodes.source.runner() as runner_source:
				runner_source.upload_file(local_temp_filepath, source_temp_filepath)
		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,
			)
			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

				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 != test_file_contents:
					problem_description = (
						u"Unexpected error: invalid content of file was transferred with '%s'\n" % (self.get_command_name(),) +
						u"Expected content: %s\n" % (test_file_contents,) +
						u"Actual content on target node: %s\n" % (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 get_command_name():
		"""Override in child classes"""
		raise NotImplementedError()

	@staticmethod
	def _create_command(key_pathname, source_runner, source_user, source_ip, source_temp_filepath, target_temp_filepath):
		"""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):
		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
		)


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):
		cmd = '/usr/bin/scp'
		args = [
			'-i', key_pathname, '-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."""

		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
						]) +
						"\nPlease increase the 'max_allowed_packet' value on the destination database server.\n"
					)
				), u""
			)
		else:
			report.add_issue(
				Problem(
					'infrastructure-max-allowed-packet', Problem.INFO,
					(
						u"{description_target_database} has enough max allowed packet for all MySQL databases.".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):
		raise NotImplementedError()

	@classmethod
	def _get_target_mysql_max_allowed_packet(cls, target_database_server):
		raise NotImplementedError()


class UnixMysqlMaxAllowedPacketChecker(MysqlMaxAllowedPacketChecker):
	@classmethod
	def _get_source_mysql_max_allowed_packet(cls, source_database_server):
		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):
		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.login(), 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):
		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):
		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.login(), 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("MySQL command '%s' with args %r returned non-zero exit code.\nStdout:\n%s\nStderr:\n%s", 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 = {
				'dataSource' : mssql_connection_data.host,
				'login' : mssql_connection_data.login,
				'pwd' : 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(), source=nodes.source.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(), source=nodes.source.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(), source=nodes.source.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(), source=nodes.source.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 (
			u"Check connection to {node} by IMAP ({port_number} port)"
		).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 (
			"Successfully connected to {node} by IMAP ({port_number} port)."
		).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 (
			u"Check connection to {node} by POP3 ({port_number} port)"
		).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 (
			"Successfully connected to {node} by POP3 ({port_number} port)."
		).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 (
			u"Check connection to {node} by IMAP ({port_number} port)"
		).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 (
			"Successfully connected to {node} by IMAP ({port_number} port)."
		).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 (
			u"Unable to copy the sample file from {source} to {target} using '{command_name}'.\n\n" +
			u"{problem_description}\n\n" +
			u"The following command has failed:\n"
			u"{failed_command_description}"
		).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 u"Successfully copied the sample file using '{command_name}' from {source} to {target}".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_unix_rsync_problem_description(self):
		return messages.UNIX_RSYNC_PROBLEM_SOLUTION

	def format_unix_rsync_invalid_file_content_problem_description(self, command_name, expected_file_contents, actual_file_contents):
		return messages.UNIX_RSYNC_PROBLEM_INVALID_TEST_FILE_CONTENTS.format(command_name=command_name, expected_file_contents=expected_file_contents, actual_file_contents=actual_file_contents)

	def format_check_log_message(self, command_name):
		return (
			u"Copy the sample file from {source} "
			u"to {target} using '{command_name}'"
		).format(
			command_name=command_name,
			source=self.nodes.source.description(),
			target=self.nodes.target.description()
		)


class Result:
	@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)


DiskSpaceDatabaseAccess = namedtuple('DiskSpaceDatabaseAccess', ('host', 'port', 'login', 'password',))
DiskSpaceDatabase = namedtuple('DiskSpaceDatabase', (
	'src_access',  # DiskSpaceDatabaseAccess (MSSQLConnectionData in case of MSSQL) for source server
	'db_target_node',  # PleskDatabaseTargetServer (PleskDatabaseTargetServer in case of PPA) for target server
	'db_name'
))
UnixDiskSpaceSourcePleskNode = namedtuple('UnixDiskSpaceSourcePleskNode', ('node', 'web_domains', 'mail_domains', '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
		Returns: 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:
					single_usage_source_mysql_db = _estimate_mysql_database_size(
						runner_source, db.src_access.host, db.src_access.port, db.src_access.login, db.src_access.password, db.db_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([
							u"- '{device}': expected to have more than {required} for migrated content, actually have {available}.".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):
		with target_node.runner() as runner:
			return sum([
				_estimate_mysql_database_size(
					runner, db.db_target_node.host(), db.db_target_node.port(),
					db.db_target_node.login(), db.db_target_node.password(),
					db.db_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):
	def check(self, source_node, mysql_databases, report):
		all_usage_source_mysql_db = [0]
		with source_node.runner() as runner:
			for db in mysql_databases:
				all_usage_source_mysql_db.append(
					_estimate_mysql_database_size(runner, db.src_access.host, db.src_access.port, db.src_access.login, db.src_access.password, db.db_name)
				)
			max_usage = max(all_usage_source_mysql_db)
			session_device, session_available = _get_available_disk_space(runner, source_node.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_node.description()
						) +
						u"- '{device}': expected to have more than {required} for migrated content, actually have {available}.".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_node.description().capitalize()
						)
					)
				), u""
			)

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


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

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

		for source_node in source_nodes:
			du_script_path_source_node = source_node.node.get_session_file_path(
				os.path.basename(du_local_script_path)
			)
			mssql_disk_usage_script_path_source_node = source_node.node.get_session_file_path(
				os.path.basename(mssql_disk_usage_local_script_path)
			)
			with source_node.node.runner() as runner_source:
				runner_source.upload_file(du_local_script_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.login, 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
						)
					)


				runner_source.upload_file(mssql_disk_usage_local_script_path, mssql_disk_usage_script_path_source_node)
				for db in source_node.mssql_databases:
					_, usage_source_mssql_db = _estimate_mssql_database_size(
						mssql_disk_usage_script_path_source_node,
						runner_source, 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
						)
					)

		self.check_with_source_usages(
			target_node, mysql_bin,
			du_local_script_path, 
			mssql_disk_usage_local_script_path, 
			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,
		du_local_script_path,
		mssql_disk_usage_local_script_path, 
		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(
			os.path.basename(du_local_script_path)
		)
		mssql_disk_usage_script_path_target_node = target_node.get_session_file_path(
			os.path.basename(mssql_disk_usage_local_script_path)
		)

		with target_node.runner() as runner_target:
			with open(du_local_script_path, 'r') as f:
				runner_target.upload_file_content(du_script_path_target_node, f.read())

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

			with open(mssql_disk_usage_local_script_path, 'r') as f:
				runner_target.upload_file_content(mssql_disk_usage_script_path_target_node, f.read())

			usages_target_mysql_db = defaultdict(int)	# { db_server: usage }
			disk_letters_mysql_db = {}			# { db_server: disk_letter }
			usages_target_mssql_db = defaultdict(int)	# { db_server: usage }
			disk_letters_mssql_db = {}			# { db_server: disk_letter }
			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.login(), 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.login(), 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_path_target_node,
					runner_target, db.db_target_node.host(), db.db_target_node.login(), 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/PPA 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)
			runner_target.remove_file(mssql_disk_usage_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_TARGET_WINDOWS.format(
								target=target_node.description()
							)
						)
					), u""
				)

	@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):
	def check(self, source_node, mysql_databases, report):
		all_usage_source_mysql_db = [0]
		for db in mysql_databases:
			with source_node.runner() as runner:
				usage_source_mysql_db = _estimate_mysql_database_size_windows_on_source_plesk(
					source_node, db.src_access.host, db.src_access.port,
					db.src_access.login, db.src_access.password, db.db_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_node.session_dir())
		with source_node.runner() as runner:
			session_available = _get_windows_disk_free_space(runner, session_disk_letter)

		logger.debug(
			messages.LOG_REQUIRED_DISK_SPACE_SOURCE, source_node.description(), session_disk_letter, max_usage
		)
		logger.debug(
			messages.LOG_AVAILABLE_DISK_SPACE_SOURCE, source_node.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_node.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_node.description().capitalize()
						)
					)
				), u""
			)


def check_database_server_connections(report, target_db_servers):
	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)
					),
				),
				u"The check was skipped."
			)
		else:
			db_report.add_issue(
				checking.Problem(
					'target_database_server_connection_ok',
					checking.Problem.INFO,
					"Successfully connected to %s" % 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', [
			'-P', # 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
			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(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/
		# for more details
		cmd = "powershell -InputFormat none -ExecutionPolicy Unrestricted -File {script_path} -hostName {host} -database {database} -login {login} -pwd {password}"
		args = dict(
			script_path=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} --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} --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("MySQL command '%s' with args %r returned non-zero exit code.\nStdout:\n%s\nStderr:\n%s", 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 <source_ip, ppa_host_id>, 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""
		)