from parallels.plesk import messages
import logging
import re
from parallels.core import MigrationError

from parallels.core.actions.base.subscription_action import SubscriptionAction
from parallels.core.checking import Report, Problem, Issue
from parallels.core.utils.plesk_utils import get_plesk_ips_with_cli
from parallels.core.utils.common import cached, group_by_id, find_first

P_HAUL_PORT = 12346
logger = logging.getLogger(__name__)


@cached
def get_sysuser_id(server, username):
	"""
	:type server: parallels.core.connections.plesk_server.PleskServer |
		parallels.core.connections.source_server.SourceServer
	:type username: string
	"""
	with server.runner() as runner:
		return runner.run('id', ['--user', username]).rstrip()


class LiveMigration(SubscriptionAction):
	def get_description(self):
		return messages.ACTION_LIVE_MIGRATION_DESCRIPTION

	def get_failure_message(self, global_context, subscription):
		"""
		:type global_context: parallels.core.global_context.GlobalMigrationContext
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		"""
		return messages.ACTION_LIVE_MIGRATION_FAILURE

	def get_repeat_count(self):
		"""
		Gets a number of attempts to perform the action
		:return: int
		"""
		return 1

	def filter_subscription(self, global_context, subscription):
		"""
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		:type global_context: parallels.core.global_context.GlobalMigrationContext
		"""
		return self._is_live_migration_option_enabled(global_context.config)

	def run(self, global_context, subscription):
		"""
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		:type global_context: parallels.core.global_context.GlobalMigrationContext
		"""

		self._pre_migration_checks(global_context, subscription)
		self._migrate_via_criu(subscription)

	@staticmethod
	@cached
	def _is_live_migration_option_enabled(config):
		"""
		:type config: ConfigParser.RawConfigParser
		"""
		return 'live-migration' in config.options('GLOBAL') \
			and config.getboolean('GLOBAL', 'live-migration')

	def _pre_migration_checks(self, global_context, subscription):
		"""
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		:type global_context: parallels.core.global_context.GlobalMigrationContext
		"""

		report = Report(messages.LIVE_MIGRATION_PRE_CHECK_REPORT_TITLE, None)
		source_report = report.subtarget(
			messages.LIVE_MIGRATION_PRE_CHECK_SOURCE_SERVER_REPORT_TITLE, subscription.web_source_server.node_id
		)
		target_report = report.subtarget(
			messages.LIVE_MIGRATION_PRE_CHECK_TARGET_SERVER_REPORT_TITLE, subscription.panel_target_server.ip()
		)

		source_issues = []
		target_issues = []
		source_issues.extend(self._check_server(subscription.web_source_server))
		target_issues.extend(self._check_server(subscription.panel_target_server))
		target_issues.extend(self._check_apache_is_stopped(subscription.panel_target_server))
		target_issues.extend(self._check_screen(subscription.panel_target_server))

		self.check_source_subscription(subscription, report)

		map(source_report.add_issue_obj, source_issues)
		map(target_report.add_issue_obj, target_issues)

		self._check_report_errors(global_context, report)

	@staticmethod
	def _migrate_via_criu(subscription):
		"""
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		"""
		def stop_ip(server, ip):
			with server.runner() as runner:
				runner.run('ip', ['addr', 'del', '%s/%s' % (ip.ip_address, ip.mask), 'dev', ip.iface])

		def start_ip(server, ip):
			with server.runner() as runner:
				runner.run('ip', ['addr', 'add', '%s/%s' % (ip.ip_address, ip.mask), 'dev', ip.iface])

		def start_p_haul_server(server):
			with server.runner() as runner:
				runner.sh(
					'setsid screen -d -L -m p.haul-service --bind-port %s >/tmp/p.haul-service.log 2>&1 &' % P_HAUL_PORT
				)

		def check_user_process(server, user_id):
			with server.runner() as runner:
				ps_status, _, _ = runner.run_unchecked('ps', ['-U', user_id])
				return ps_status == 0

		def migrate_user(server, user_id):
			with server.runner() as runner:
				runner.sh(
					'p.haul -v 4 --keep-images --migrate-tcp --migrate-ext-unix-sk --migrate-files '
					'--migrate-as-shell-job --port %s uid %s %s' % (
						P_HAUL_PORT,
						user_id,
						subscription.panel_target_server.ip()
					)
				)
		source_ip = find_first(
			get_plesk_ips_with_cli(subscription.web_source_server),
			lambda x: x.ip_address == subscription.target_web_ip
		)
		target_ip = find_first(
			get_plesk_ips_with_cli(subscription.panel_target_server),
			lambda x: x.ip_address == subscription.target_web_ip
		)

		user_id = get_sysuser_id(
			subscription.web_source_server,
			subscription.converted_dump.get_phosting_sysuser_name()
		)
		if not check_user_process(subscription.web_source_server, user_id):
			return
		stop_ip(subscription.web_source_server, source_ip)
		start_ip(subscription.panel_target_server, target_ip)
		start_p_haul_server(subscription.panel_target_server)
		migrate_user(subscription.web_source_server, user_id)

	# ===================================================== Checks =====================================================

	@cached
	def _check_server(self, server):
		"""
		:type server: parallels.core.connections.plesk_server.PleskServer |
		parallels.core.connections.source_server.SourceServer
		"""
		issues = []
		self._check_selinux(server, issues)
		self._check_apache_modules(server, issues)
		self._check_criu(server, issues)
		self._check_phaul(server, issues)
		return issues

	def check_source_subscription(self, subscription, report):
		"""
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		:type report: parallels.core.checking.Report
		"""
		self._check_subscription_ip(subscription, report)
		self._check_sys_user(subscription, report)
		self._check_domains(subscription, report)

	# ------------------------------------------------- Server checks --------------------------------------------------

	@staticmethod
	def _check_selinux(server, issues):
		"""
		:type server: parallels.core.connections.plesk_server.PleskServer |
		parallels.core.connections.source_server.SourceServer
		:type issues: list[parallels.core.checking.Issue]
		"""
		logger.debug(messages.LIVE_MIGRATION_CHECKING_SELINUX)

		with server.runner() as runner:
			selinux = runner.run('getenforce', [])  # raised an exception w/o []
			if selinux.strip().lower() != 'disabled':
				issues.append(
					Issue(
						Problem(
							'selinux_is_enabled',
							Problem.ERROR,
							messages.LIVE_MIGRATION_SELINUX_ENABLED_ISSUE),
						messages.LIVE_MIGRATION_SELINUX_ENABLED_SOLUTION)
				)

	@staticmethod
	def _check_apache_modules(server, issues):
		"""
		:type server: parallels.core.connections.plesk_server.PleskServer |
		parallels.core.connections.source_server.SourceServer
		:type issues: list[parallels.core.checking.Issue]
		"""
		logger.debug(messages.LIVE_MIGRATION_CHECKING_APACHE_MODULES)

		excess_modules = {'mod_ssl', 'fast_cgi'}
		needed_modules = {'ruid2_module'}

		with server.runner() as runner:
			# Example findall string: 'setenvif_module (shared)\n'
			modules = set(re.findall(r'([\w_]+)\s*\(\w+\)', runner.run('apachectl', ['-M'])))
			modules_to_drop = excess_modules & modules
			modules_to_install = needed_modules - modules

		if modules_to_drop:
			issues.append(
				Issue(
					Problem(
						'modules_to_disable',
						Problem.ERROR,
						messages.LIVE_MIGRATION_UNSUPPORTED_APACHE_MODULES_ENABLED_ISSUE % u', '.join(modules_to_drop)
					),
					''
				)
			)

		if modules_to_install:
			issues.append(
				Issue(
					Problem(
						'modules_to_install',
						Problem.ERROR,
						messages.LIVE_MIGRATION_MODULES_NOT_INSTALLED_ISSUE %
						u', '.join(modules_to_drop)
					),
					''
				)
			)

	@staticmethod
	def _check_criu(server, issues):
		"""
		:type server: parallels.core.connections.plesk_server.PleskServer |
		parallels.core.connections.source_server.SourceServer
		:type issues: list[parallels.core.checking.Issue]
		"""
		logger.debug(messages.LIVE_MIGRATION_CHECKING_CRIU)

		with server.runner() as runner:
			status, criu, _ = runner.run_unchecked('which', ['criu'])
			if status != 0:
				issues.append(
					Issue(
						Problem(
							'install_criu',
							Problem.ERROR,
							messages.LIVE_MIGRATION_CRIU_TOOLS_NOT_INSTALLED_ISSUE
						),
						messages.LIVE_MIGRATION_CRIU_TOOLS_NOT_INSTALLED_SOLUTION
					)
				)
			else:
				status, _, err = runner.run_unchecked(criu.rstrip(), ['check'])
				if status != 0:
					issues.append(
						Issue(
							Problem(
								'bad_kernel',
								Problem.ERROR,
								messages.LIVE_MIGRATION_KERNEL_DOES_NOT_SUPPORT_CRIU_ISSUE % err
							),
							messages.LIVE_MIGRATION_KERNEL_DOES_NOT_SUPPORT_CRIU_SOLUTION
						)
					)

	def _check_phaul(self, server, issues):
		"""
		:type server: parallels.core.connections.plesk_server.PleskServer |
		parallels.core.connections.source_server.SourceServer
		:type issues: list[parallels.core.checking.Issue]
		"""
		logger.debug(messages.LIVE_MIGRATION_CHECKING_PHAUL)
		self._check_tool(server, ['p.haul', 'p.haul-service'], issues)

	@staticmethod
	@cached
	def _check_apache_is_stopped(server):
		"""
		:type server: parallels.core.connections.plesk_server.PleskServer |
		parallels.core.connections.source_server.SourceServer
		:rtype: list[parallels.core.checking.Issue]
		"""
		issues = []
		with server.runner() as runner:
			status, _, _ = runner.run_unchecked('service', ['httpd', 'status'])
			if status == 0:
				issues.append(
					Issue(
						Problem(
							'apache_runned',
							Problem.ERROR,
							messages.LIVE_MIGRATION_APACHE_STARTED_ISSUE
						),
						messages.LIVE_MIGRATION_APACHE_STARTED_SOLUTION
					)
				)
		return issues

	@cached
	def _check_screen(self, server):
		"""
		:type server: parallels.core.connections.plesk_server.PleskServer |
		parallels.core.connections.source_server.SourceServer
		:rtype: list[parallels.core.checking.Issue]
		"""

		issues = []
		self._check_tool(server, ['screen'], issues, check_configure=False)
		return issues

	# ---------------------------------------------- Subscription checks -----------------------------------------------
	@staticmethod
	def _check_subscription_ip(subscription, report):
		"""
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		:type report: parallels.core.checking.Report
		"""
		if subscription.raw_dump.web_ips.v4_type != 'exclusive':
			subscription.add_report_issue(
				report,
				Problem(
					'wrong_ip_type',
					Problem.ERROR,
					messages.LIVE_MIGRATION_IP_ADDRESS_NOT_EXCLUSIVE_ISSUE
				),
				messages.LIVE_MIGRATION_IP_ADDRESS_NOT_EXCLUSIVE_SOLUTION
			)
			return

		if subscription.target_web_ip != subscription.raw_dump.web_ips.v4:
			subscription.add_report_issue(
				report,
				Problem(
					'unequal_ip',
					Problem.ERROR,
					messages.LIVE_MIGRATION_NOT_EQUAL_IPS_ISSUE),
				messages.LIVE_MIGRATION_NOT_EQUAL_IPS_SOLUTION
			)
			return

	@staticmethod
	def _check_php_handler_type(domain):
		if domain.scripting is None:
			return False

		options_by_name = group_by_id(domain.scripting.options, lambda o: o.name)
		php_handler_type = options_by_name.get('php_handler_type')
		return php_handler_type is not None and php_handler_type.value == 'module'

	@staticmethod
	def _check_ruid2(subscription, domain):
		"""
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		"""
		with subscription.web_source_server.runner() as runner:
			exit_code, _, _ = runner.run_unchecked(
				'grep', ['-q', 'RUidGid', '/var/www/vhosts/system/%s/conf/vhost.conf' % domain.name]
			)
			return exit_code == 0

	@staticmethod
	def _check_sys_user(subscription, report):
		"""
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		:type report: parallels.core.checking.Report
		"""

		sysuser_name = subscription.converted_dump.get_phosting_sysuser_name()
		target_sysuser_id = get_sysuser_id(subscription.panel_target_server, sysuser_name)
		source_sysuser_id = get_sysuser_id(subscription.web_source_server, sysuser_name)
		if target_sysuser_id != source_sysuser_id:
			subscription.add_report_issue(
				report,
				Problem(
					'unequal_sysuser_id',
					Problem.ERROR,
					messages.LIVE_MIGRATION_NOT_EQUAL_SYSUSER_ID_ISSUE
				),
				messages.LIVE_MIGRATION_NOT_EQUAL_SYSUSER_ID_SOLUTION
			)

	def _check_domains(self, subscription, report):
		"""
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		:type report: parallels.core.checking.Report
		"""
		exist_domain_to_migration = False
		for domain in subscription.converted_dump.iter_domains():
			if not self._check_php_handler_type(domain):
				subscription.add_report_issue(
					report,
					Problem(
						'wrong_php_handler',
						Problem.WARNING,
						messages.LIVE_MIGRATION_UNSUPPORTED_PHP_HANDLER_TYPE_ISSUE % domain.name
					),
					messages.LIVE_MIGRATION_UNSUPPORTED_PHP_HANDLER_TYPE_SOLUTION)
				continue
			if not self._check_ruid2(subscription, domain):
				subscription.add_report_issue(
					report,
					Problem(
						'disabled_mod_ruid2',
						Problem.WARNING,
						messages.LIVE_MIGRATION_MOD_RUID2_NOT_ENABLED_ISSUE % domain.name
					),
					messages.LIVE_MIGRATION_MOD_RUID2_NOT_ENABLED_SOLUTION % domain.name
				)
				continue
			exist_domain_to_migration = True

		if not exist_domain_to_migration:
			subscription.add_report_issue(
				report,
				Problem(
					'wrong_php_handler',
					Problem.ERROR,
					messages.LIVE_MIGRATION_NO_SUITABLE_DOMAIN_ISSUE
				),
				''
			)

	# ==================================================================================================================

	@staticmethod
	def _check_tool(server, tools_to_check, issues, check_configure=True):
		"""
		:type server: parallels.core.connections.plesk_server.PleskServer |
		parallels.core.connections.source_server.SourceServer
		:type tools_to_check: list[str]
		:type issues: list[parallels.core.checking.Issue]
		"""
		with server.runner() as runner:
			for tool in tools_to_check:
				status, tool_path, _ = runner.run_unchecked('which', [tool])
				if status != 0:
					issues.append(
						Issue(
							Problem(
								'tool_not_installed',
								Problem.ERROR,
								messages.LIVE_MIGRATION_TOOL_NOT_INSTALLED_ISSUE % tool
							),
							''
						)
					)

				if not check_configure:
					return

				status, _, err = runner.run_unchecked(tool_path.rstrip(), ['--help'])
				if status != 0:
					issues.append(
						Issue(
							Problem(
								'tool_not_configured',
								Problem.ERROR,
								messages.LIVE_MIGRATION_TOOL_NOT_CONFIGURED_ISSUE % (tool, err)
							),
							''
						)
					)

	@staticmethod
	def _check_report_errors(global_context, report):
		"""Check report and exit in case of errors

		:type global_context: parallels.core.global_context.GlobalMigrationContext
		:type report parallels.core.checking.Report
		"""
		if report.has_errors():
			global_context.migrator.print_report(
				report, "convert_report_tree", show_no_issue_branches=False
			)
			raise MigrationError(messages.LIVE_MIGRATION_CRITICAL_ERRORS)
