"""
Single interface for running commands via different transports (for example, SSH, Windows HCL, Unix HCL).
"""
import logging
import pipes
import os
import shutil
import ntpath
from contextlib import closing
import errno

from parallels.utils import poll_data, polling_intervals, safe_string_repr, strip_multiline_spaces, cached
from parallels.common.registry import Registry
from parallels.common.utils.steps_profiler import sleep
from parallels.common.utils import poa_utils, windows_utils, ssh_utils, unix_utils
from parallels.common.utils import windows_thirdparty
from parallels.common import local_command_exec
from parallels.common import MigrationError
from parallels.common.utils.windows_agent_pool import WindowsAgentPool
from parallels.common.utils.windows_utils import check_windows_national_symbols_command
from parallels.common.utils.steps_profiler import get_default_steps_profiler
from parallels.utils.logging import PositionalArgCommand, NamedArgCommand

logger = logging.getLogger(__name__)
profiler = get_default_steps_profiler()


class HclRunnerException(MigrationError):
	def __init__(self, message, stdout='', stderr='', host_description='', cause=None):
		super(MigrationError, self).__init__(message)
		self.stdout = stdout
		self.stderr = stderr
		self.host_description = host_description
		self.cause = cause


class SSHRunnerException(MigrationError):
	def __init__(self, message, command_str=None, stdout=None, stderr=None):
		super(MigrationError, self).__init__(message)
		self.command_str = command_str
		self.stdout = stdout
		self.stderr = stderr


class BaseRunnerException(MigrationError):
	def __init__(self, message):
		super(MigrationError, self).__init__(message)


class ConnectionCheckError(MigrationError):
	pass


class WinexeRunnerException(MigrationError):
	def __init__(self, message, command_str=None, full_command_str=None, stdout=None, stderr=None):
		super(MigrationError, self).__init__(message)
		self.command_str = command_str
		self.full_command_str = full_command_str
		self.stdout = stdout
		self.stderr = stderr


class BaseRunner(object):
	def run_unchecked(self, cmd, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		"""Run command with specified args. Arguments are properly escaped."""
		raise NotImplementedError()

	def sh_unchecked(
			self, cmd_str, args=None, stdin_content=None, output_codepage=None,
			error_policy='strict', env=None):
		"""Substitute format fields in 'cmd_str', then execute the command string.
		
		For Unix: whole command is executed, then if you run

			"runner.sh('ls *')"

		, '*' will be substituted by shell, while if you run

			"runner.sh('ls {file}', dict(file='*'))"

		, '*' will be escaped, much like "run('ls', ['*'])".
		
		For Windows: there are no general command arguments parsing rules, and
		'*' is subsitituded by each particular program, this function differs
		from run_unchecked only by a way to substitute arguments.

		If None is passed as args, no command formatting is performed, command
		run as is.

		Returns a tuple (exit_code, stdout, stderr)
		"""
		raise NotImplementedError()

	def run(self, cmd, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		"""The same as run_unchecker(), but checks exit code and returns only stdout"""
		exit_code, stdout, stderr = self.run_unchecked(cmd, args, stdin_content, output_codepage, error_policy, env)
		if exit_code != 0:
			raise BaseRunnerException(
				u"Command %s with arguments %r failed with exit code %d:\n"
				u"stdout: %s\nstderr: %s"
				% (cmd, args, exit_code, safe_string_repr(stdout), safe_string_repr(stderr))
			)
		return stdout 

	def sh(self, cmd, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		"""The same as sh_unchecked(), but checks exit code and returns only stdout"""
		exit_code, stdout, stderr = self.sh_unchecked(cmd, args, stdin_content, output_codepage, error_policy, env)
		if exit_code != 0:
			raise BaseRunnerException(
				u"Command %s with arguments %r failed with exit code %d:\n"
				u"stdout: %s\nstderr: %s"
				% (cmd, args, exit_code, safe_string_repr(stdout), safe_string_repr(stderr))
			)
		return stdout

	def sh_multiple_attempts(
			self, cmd_str, args=None, stdin=None, codepage=None,
			error_policy='strict', env=None, max_attempts=5,
			interval_between_attempts=10):
		"""Run command with several attempts; check exit code; return only stdout."""
		from parallels.common.utils.migrator_utils import get_bleeping_logger
		blogger = get_bleeping_logger(logger)
		command = self._get_command_object(cmd_str, args, self._get_command_str)
		for attempt in range(0, max_attempts):
			blogger.debug(u'Attempting to run "%s"', command)
			exit_code, stdout, stderr = self.sh_unchecked(
					cmd_str, args, stdin, codepage, error_policy, env)

			if exit_code == 0:
				if attempt > 0:
					blogger.info(
						'Command "%s" was executed successfully after'
						' %d attempt(s).', command, attempt+1)
				return stdout
			elif attempt >= max_attempts - 1:
				failed_cmd = blogger.format('%s', command)
				self._raise_runner_exception(failed_cmd, stdout, stderr)
			else:
				blogger.error(
					'Failed to execute a remote command, retry in'
					' %d seconds' % interval_between_attempts)
				sleep(interval_between_attempts, 'Retry executing command')

	def _get_command_object(self, command, args, serializer):
		if isinstance(args, dict):
			command_class = NamedArgCommand
		elif isinstance(args, list):
			command_class = PositionalArgCommand
		else:
			if args is None:
				return command
			else:
				return '%s %s' % (command, args)
		return command_class(command, args, serializer)

	def _raise_runner_exception(self, cmd_str, stdout, stderr):
		raise NotImplementedError()

	def upload_file_content(self, filename, content):
		raise NotImplementedError()

	def upload_file(self, local_filename, remote_filename):
		raise NotImplementedError()

	def get_file(self, remote_filename, local_filename):
		raise NotImplementedError()

	def get_file_contents(self, remote_filename):
		raise NotImplementedError()

	def remove_file(self, filename):
		"""Remove file. If file does not exist - silently do nothing.

		:type filename: basestring
		"""
		raise NotImplementedError()

	def remove_directory(self, directory, is_remove_root=True):
		raise NotImplementedError()

	def clean_directory(self, directory):
		self.remove_directory(directory, False)

	def file_exists(self, filename):
		raise NotImplementedError()

	def mkdir(self, dirname):
		raise NotImplementedError()

	def check(self, server_description):
		"""Check connection with that runner

		:type server_description: basestring
		:rtype None
		"""
		raise NotImplementedError()

	def get_files_list(self, path):
		raise NotImplementedError()

	@staticmethod
	def _get_command_str(cmd_str, args=None):
		raise NotImplementedError()


class WindowsRunner(BaseRunner):
	"""Add codepage detection to migrator's Windows CMD interface classes."""
	@property
	@cached
	def codepage(self):
		"""Determine code page used by Windows CMD.

		'CHCP' utility returns Windows codepage, for example:
		"Active code page: 123"

		Python codec name is derived from 'chcp' output by adding 'cp' to code
		page number, for example: '866' -> 'cp866'
		
		Returns:
		    Python codec name to be used for decoding CMD output.
		"""

		code, stdout, stderr = self._run_command_and_decode_output(
				"cmd.exe /c CHCP", 'ascii', error_policy='ignore')
		if code != 0:
			raise BaseRunnerException(
				u"Unable to determine Windows CMD code page:\nstdout: %s\nstderr: %s"
					% (safe_string_repr(stdout), safe_string_repr(stderr))
			)
		else:
			codepage =  u"cp%s" % stdout.split()[-1].strip('.')
			logger.debug("Detected CMD codepage: %s" % codepage)
		return codepage

	def remove_file(self, filename):
		self.sh('cmd /c del {filename}', dict(filename=filename))

	def remove_directory(self, directory, is_remove_root=True):
		# remove all files in a directory recursively
		self.sh('cmd /c del {directory} /s /q', dict(directory=directory))
		# remove all directories recursively
		directories_to_delete = directory if is_remove_root else ntpath.join(directory, '*')
		self.sh(
			u'cmd /c for /d %p in ({directories_to_delete}) do rmdir "%p" /s /q',
			dict(directories_to_delete=directories_to_delete)
		)

	def _run_command_and_decode_output(
			self, cmd_str, output_codepage=None, error_policy='strict', env=None):
		"""Run a command, return its output decoded to UTF."""
		raise NotImplementedError()

	def file_exists(self, filename):
		return windows_utils.file_exists(self, filename)

class LocalUnixRunner(BaseRunner):
	"""Execute commands on local server"""

	@property
	def codepage(self):
		return 'utf-8'

	def run_unchecked(self, cmd, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		command = [cmd] + ([] if args is None else args)
		codepage = output_codepage if output_codepage else self.codepage
		return local_command_exec(command, stdin_content, codepage, error_policy, env)

	def sh_unchecked(
			self, cmd_str, args=None, stdin_content=None, output_codepage=None,
			error_policy='strict', env=None):
		if args is None:
			command = ['/bin/sh', '-c', cmd_str]
		else:
			formatted_cmd = unix_utils.format_command(cmd_str, **args)
			command = [ '/bin/sh', '-c', formatted_cmd]
		codepage = output_codepage if output_codepage else self.codepage
		return local_command_exec(command, stdin_content, codepage, error_policy, env)

	def mkdir(self, dirname):
		return self.sh('mkdir -p {dirname}', dict(dirname=dirname))

	def upload_file_content(self, filename, content):
		with open(filename, "wb") as fp:
			fp.write(content)

	def upload_file(self, local_filename, remote_filename):
		if local_filename != remote_filename:
			shutil.copy2(local_filename, remote_filename)

	def get_file(self, remote_filename, local_filename):
		if local_filename != remote_filename:
			shutil.copy2(remote_filename, local_filename)

	def get_file_contents(self, remote_filename):
		with open(remote_filename, "rb") as fp:
			return fp.read().decode('utf-8')

	def remove_file(self, filename):
		os.remove(filename)

	def check(self, server_description):
		# No need to check local execution, it should never fail
		pass

	def get_files_list(self, path):
		return unix_utils.get_files_list(self, path)

	def file_exists(self, filename):
		return os.path.exists(filename)


class LocalWindowsRunner(WindowsRunner):
	"""Execute commands on local Windows server"""

	def __init__(self, disable_profiling=False):
		self._disable_profiling = disable_profiling

	@staticmethod
	@cached
	def get_instance(disable_profiling=False):
		return LocalWindowsRunner(disable_profiling)

	def _run_command_and_decode_output(
			self, cmd_str, output_codepage=None, error_policy='strict', env=None):
		codepage = output_codepage if output_codepage else self.codepage
		return local_command_exec(cmd_str, None, codepage, error_policy, env)

	def sh_unchecked(
			self, cmd_str, args=None, stdin_content=None, output_codepage=None,
			error_policy='strict', env=None):
		if args is not None:
			cmd_str = windows_utils.format_command(cmd_str, **args)
		logger.debug("Execute a command on local server: '%s'" % (cmd_str))
		codepage = output_codepage if output_codepage else self.codepage

		if not self._disable_profiling:
			with profiler.measure_command_call(
				cmd_str, 'Local server'
			):
				return local_command_exec(cmd_str, stdin_content, codepage, error_policy, env)
		else:
			return local_command_exec(cmd_str, stdin_content, codepage, error_policy, env)

	def run_unchecked(
			self, cmd, args=None, stdin_content=None, output_codepage=None,
			error_policy='strict', env=None):
		command = windows_utils.format_command_list(cmd, args)
		args = None
		return self.sh_unchecked(
				command, args, stdin_content, output_codepage, error_policy, env)

	def upload_file(self, local_filename, remote_filename):
		shutil.copy2(local_filename, remote_filename)

	def upload_file_content(self, filename, content):
		with open(filename, "wb") as fp:
			fp.write(content)

	def get_file(self, remote_filename, local_filename):
		if local_filename != remote_filename:
			logger.debug("Copy local file '%s' to '%s'", remote_filename, local_filename)
			shutil.copy2(remote_filename, local_filename)

	def get_file_contents(self, remote_filename):
		with open(remote_filename, 'rb') as fp:
			return fp.read().decode('utf-8')

	def mkdir(self, dirname):
		self.sh('cmd /c if not exist {dirname} ( md {dirname} )', dict(dirname=dirname))

	def remove_file(self, filename):
		try:
			os.remove(filename)
		except OSError as e:
			# if file does not exist - just skip
			if e.errno != errno.ENOENT:
				raise

	def remove_directory(self, directory, is_remove_root=True):
		for root, dirs, files in os.walk(directory):
			for name in files:
				self.remove_file(ntpath.join(directory, name))
			for name in dirs:
				self.remove_directory(ntpath.join(directory, name))
			if is_remove_root:
				os.rmdir(root)

	def check(self, server_description):
		# No need to check local execution, it should never fail
		pass

class WindowsHclRunner(WindowsRunner):
	def __init__(self, ppa_runner, poa_host_id, host_description):
		self.ppa_runner = ppa_runner
		self.poa_host_id = poa_host_id
		self.host_description = host_description

	def run(self, cmd, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		"""The same as run_unchecked(), but checks exit code and returns only stdout"""
		log_cmd_str = self._get_cmd_str_listargs(cmd, args)

		def run_func():
			exit_code, stdout, stderr = self._run_unchecked(cmd, args, stdin_content, output_codepage, error_policy, env)
			if exit_code != 0:
				raise HclRunnerException(strip_multiline_spaces("""
					The command "{command}" executed at {host} returned a non-zero exit code.
					============================================================================
					Stderr:\n{stderr}
					============================================================================
					Stdout:\n{stdout}
					============================================================================
					1. If this happened because of a network issue, re-run the migration tool.
					2. To investigate the issue, execute an HCL request and look at the result:
					2.1. Log in to the PPA management node.
					2.2. Create an XML file with an HCL request (/tmp/tmp-hcl for example):
					{hcl_script}
					2.3. Run the command "/usr/local/pem/bin/pleskd_ctl -f /usr/local/pem/etc/pleskd.props processHCL /tmp/tmp-hcl {host_id}" 
					""").format(
						stderr=stderr,
						stdout=stdout,
						command=log_cmd_str,
						host=self._get_host_description(),
						host_id=self.poa_host_id,
						hcl_script=poa_utils.get_windows_hcl_script(log_cmd_str, env)
					),
					stdout,
					stderr
				)
			return stdout 

		return hcl_execute_multiple_attempts(run_func, self._get_host_description(), log_cmd_str)

	def run_unchecked(self, cmd, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		log_cmd_str = self._get_cmd_str_listargs(cmd, args)
		def run_func():
			return self._run_unchecked(cmd, args, stdin_content, output_codepage, error_policy, env)
		log_cmd_str = " ".join([windows_utils.quote_arg(arg) for arg in [cmd] + ([] if args is None else args)])
		return hcl_execute_multiple_attempts(run_func, self._get_host_description(), log_cmd_str)

	def _run_unchecked(self, cmd, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		if stdin_content is not None:
			raise NotImplementedError(u"This runner does not support stdin_content parameter")

		cmd_str = " ".join([windows_utils.quote_arg(arg) for arg in [cmd] + ([] if args is None else args)])
		return self._run_command_and_decode_output(cmd_str, output_codepage, error_policy, env)

	def sh(self, cmd_str, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		"""The same as sh_unchecked(), but checks exit code and returns only stdout"""
		log_cmd_str = self._get_cmd_str_formatargs(cmd_str, args)

		def run_func():
			exit_code, stdout, stderr = self.sh_unchecked(cmd_str, args, stdin_content, output_codepage, error_policy, env)
			if exit_code != 0:
				logger.debug(u"Command '%s' failed with '%s' exit code, output: '%s', stderr: '%s'." % (log_cmd_str, exit_code, safe_string_repr(stdout), safe_string_repr(stderr)))
				raise HclRunnerException(strip_multiline_spaces("""
					The command "{command}" executed at {host} returned a non-zero exit code.
					============================================================================
					Stderr:\n{stderr}
					============================================================================
					Stdout:\n{stdout}
					============================================================================
					1. If this happened because of a network issue, re-run the migration tool.
					2. To investigate the issue, execute an HCL request and look at the result:
					2.1. Log in to the PPA management node.
					2.2. Create an XML file with an HCL request (/tmp/tmp-hcl for example):
					{hcl_script}
					2.3. Run the command "/usr/local/pem/bin/pleskd_ctl -f /usr/local/pem/etc/pleskd.props processHCL /tmp/tmp-hcl {host_id}" 
					""").format(
						stderr=stderr,
						stdout=stdout,
						command=log_cmd_str,
						host=self._get_host_description(),
						host_id=self.poa_host_id,
						hcl_script=poa_utils.get_windows_hcl_script(log_cmd_str, env)
					),
					stdout,
					stderr
				)
			return stdout

		return hcl_execute_multiple_attempts(run_func, self._get_host_description(), log_cmd_str)

	def sh_unchecked(self, cmd_str, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		def run_func():
			return self._sh_unchecked(cmd_str, args, stdin_content, output_codepage, error_policy, env)
		return hcl_execute_multiple_attempts(run_func, self._get_host_description(), self._get_cmd_str_formatargs(cmd_str, args))

	def _sh_unchecked(self, cmd_str, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		if stdin_content is not None:
			raise NotImplementedError(u"This runner does not support stdin_content parameter")

		if args is not None:
			cmd_str = windows_utils.format_command(cmd_str, **args)
		return self._run_command_and_decode_output(cmd_str, output_codepage, error_policy, env)

	def _run_command_and_decode_output(self, cmd_str, output_codepage=None, error_policy='strict', env=None):
		"""Run a command, return its output decoded from 'codepage' to UTF."""
		check_windows_national_symbols_command(cmd_str)
		codepage = output_codepage if output_codepage else self.codepage
		try:
			with profiler.measure_command_call(
				cmd_str, self._get_host_description()
			):
				return poa_utils.windows_exec_unchecked(
					self.ppa_runner, self.poa_host_id, cmd_str,
					codepage=codepage, error_policy=error_policy, env=env
				)
		except poa_utils.HclPleskdCtlNonZeroExitCodeError as e:
			logger.debug(u'Exception:', exc_info=e)
			raise HclRunnerException(strip_multiline_spaces("""
				Failed to execute the command "{command}" at {host}: pleskd_ctl returned a non-zero exit code when executing HCL request. 
				============================================================================
				Stderr:\n{stderr}
				============================================================================
				Stdout:\n{stdout}
				============================================================================
				1. If this happened because of a network issue, re-run the migration tool.
				2. To investigate the issue, execute an HCL request and look at the result:
				2.1. Log in to the PPA management node.
				2.2. Create an XML file with an HCL request (/tmp/tmp-hcl for example):
				{hcl_script}
				2.3. Run the command "/usr/local/pem/bin/pleskd_ctl -f /usr/local/pem/etc/pleskd.props processHCL /tmp/tmp-hcl {host_id}" 
				""").format(
					stderr=e.stderr,
					stdout=e.stdout,
					command=cmd_str,
					host=self._get_host_description(),
					host_id=self.poa_host_id,
					hcl_script=poa_utils.get_windows_hcl_script(cmd_str, env)
				),
				'',
				str(e),
				cause=e,
				host_description=self._get_host_description()
			)

	def upload_file(self, local_filename, remote_filename):
		with open(local_filename, 'rb') as fp:
			return poa_utils.hcl_upload(
				self.ppa_runner, self.poa_host_id,
				remote_filename, fp.read()
			)
	
	def upload_file_content(self, filename, content):
		return poa_utils.hcl_upload(self.ppa_runner, self.poa_host_id, filename, content)

	def get_file(self, remote_filename, local_filename):
		with open(local_filename, 'wb') as fp:
			fp.write(poa_utils.hcl_get_file_contents(self.ppa_runner, self.poa_host_id, remote_filename))

	def get_file_contents(self, remote_filename):
		return poa_utils.hcl_get_file_contents(self.ppa_runner, self.poa_host_id, remote_filename)

	def mkdir(self, dirname):
		self.sh('cmd /c if not exist {dirname} ( md {dirname} )', dict(dirname=dirname))

	def _get_host_description(self):
		return self.host_description if self.host_description is not None else "PPA service node #%s" % (self.poa_host_id,)

	@staticmethod
	def _get_cmd_str_listargs(cmd, args):
		return " ".join([windows_utils.quote_arg(arg) for arg in [cmd] + ([] if args is None else args)])

	@staticmethod
	def _get_cmd_str_formatargs(cmd_str, args):
		if args is not None:
			return windows_utils.format_command(cmd_str, **args)
		else:
			return cmd_str

class UnixHclRunner(BaseRunner):
	def __init__(self, ppa_runner, poa_host_id, host_description=None):
		self.ppa_runner = ppa_runner
		self.poa_host_id = poa_host_id
		self.host_description = host_description

	def run(self, cmd, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		"""The same as run_unchecker(), but checks exit code and returns only stdout"""
		def run_func():
			log_cmd_str = self._get_cmd_str_listargs(cmd, args)
			exit_code, stdout, stderr = self._run_unchecked(cmd, args, stdin_content, output_codepage, error_policy, env)
			if exit_code != 0:
				logger.debug(u"Command '%s' failed with '%s' exit code, output: '%s', stderr: '%s'." % (log_cmd_str, exit_code, safe_string_repr(stdout), safe_string_repr(stderr)))
				raise HclRunnerException(strip_multiline_spaces("""
					The command "{log_command}" executed at {host} returned a non-zero exit code. 
					============================================================================
					Stderr:\n{stderr}
					============================================================================
					Stdout:\n{stdout}
					============================================================================
					1. If this happened because of a network issue, re-run the migration tool.
					2. To investigate the issue, execute an HCL request and look at the result:
					2.1. Log in to the PPA management node.
					2.2. Create an XML file with an HCL request (/tmp/tmp-hcl for example):
					{hcl_script}
					2.3. Run the command "/usr/local/pem/bin/pleskd_ctl -f /usr/local/pem/etc/pleskd.props processHCL /tmp/tmp-hcl {host_id}"
					""").format(
						stdout=stdout,
						stderr=stderr,
						log_command=log_cmd_str,
						host=self._get_host_description(),
						host_id=self.poa_host_id,
						hcl_script=poa_utils.get_unix_hcl_script(cmd, args, stdin_content, env)
					),
					stdout,
					stderr
				)
			return stdout 

		return hcl_execute_multiple_attempts(run_func, self._get_host_description(), self._get_cmd_str_listargs(cmd, args))

	def run_unchecked(self, cmd, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		def run_func():
			return self._run_unchecked(cmd, args, stdin_content, output_codepage, error_policy, env)

		return hcl_execute_multiple_attempts(run_func, self._get_host_description(), self._get_cmd_str_listargs(cmd, args))

	def _run_unchecked(self, cmd, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		log_cmd_str = self._get_cmd_str_listargs(cmd, args)

		try:
			with profiler.measure_command_call(
				log_cmd_str, self._get_host_description()
			):
				return poa_utils.unix_exec_unchecked(self.ppa_runner, self.poa_host_id, cmd, args, stdin_content, env)
		except poa_utils.HclPleskdCtlNonZeroExitCodeError as e:
			logger.debug(u'Exception:', exc_info=e)
			raise HclRunnerException(strip_multiline_spaces("""
				Failed to execute the command "{command}" at {host}. 
				============================================================================
				Stderr:\n{stderr}
				============================================================================
				Stdout:\n{stdout}
				============================================================================
				1. If this happened because of a network issue, re-run the migration tool.
				2. To investigate the issue, execute an HCL request and look at the result:
				2.1. Log in to the PPA management node.
				2.2. Create an XML file with an HCL request (/tmp/tmp-hcl for example):
				{hcl_script}
				2.3. Run the command "/usr/local/pem/bin/pleskd_ctl -f /usr/local/pem/etc/pleskd.props processHCL /tmp/tmp-hcl {host_id}"
				""").format(
					stderr=e.stderr,
					stdout=e.stdout,
					command=log_cmd_str,
					host=self._get_host_description(),
					host_id=self.poa_host_id,
					hcl_script=poa_utils.get_unix_hcl_script(cmd, args, stdin_content, env)
				),
				'',
				str(e),
				cause=e,
				host_description=self._get_host_description()
			)

	def sh(self, cmd_str, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		"""The same as sh_unchecked(), but checks exit code and returns only stdout"""
		log_cmd_str = self._get_cmd_str_formatargs(cmd_str, args)

		def run_func():
			exit_code, stdout, stderr = self._sh_unchecked(cmd_str, args, stdin_content, output_codepage, error_policy, env)
			if exit_code != 0:
				logger.debug(u"Command '%s' failed with '%s' exit code, output: '%s', stderr: '%s'." % (log_cmd_str, exit_code, safe_string_repr(stdout), safe_string_repr(stderr)))
				raise HclRunnerException(strip_multiline_spaces("""
					The command "{log_command}" executed at {host} returned a non-zero exit code.
					============================================================================
					Stderr:\n{stderr}
					============================================================================
					Stdout:\n{stdout}
					============================================================================
					1. If this happened because of a network issue, re-run the migration tool.
					2. To investigate the issue, execute an HCL request and look at the result:
					2.1. Log in to the PPA management node.
					2.2. Create an XML file with an HCL request (/tmp/tmp-hcl for example):
					{hcl_script}
					2.3. Run the command "/usr/local/pem/bin/pleskd_ctl -f /usr/local/pem/etc/pleskd.props processHCL /tmp/tmp-hcl {host_id}" 
					""").format(
						stdout=stdout,
						stderr=stderr,
						log_command=log_cmd_str,
						host=self._get_host_description(),
						host_id=self.poa_host_id,
						hcl_script=poa_utils.get_unix_hcl_script(log_cmd_str, [], stdin_content, env)
					),
					stdout,
					stderr
				)
			return stdout

		return hcl_execute_multiple_attempts(run_func, self._get_host_description(), log_cmd_str)

	def sh_unchecked(self, cmd_str, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		def run_func():
			return self._sh_unchecked(cmd_str, args, stdin_content, output_codepage, error_policy, env)

		return hcl_execute_multiple_attempts(run_func, self._get_host_description(), self._get_cmd_str_formatargs(cmd_str, args))

	def _sh_unchecked(self, cmd_str, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		run_cmd, run_args = self._get_command_list(cmd_str, args)

		return self._run_unchecked(run_cmd, run_args, stdin_content, output_codepage, error_policy, env)

	def _get_command_list(self, cmd_str, args):
		if args is not None:
			cmd_str = unix_utils.format_command(cmd_str, **args)
		return '/bin/sh', ['-c', cmd_str]

	def get_file(self, remote_filename, local_filename):
		with open(local_filename, 'wb') as fp:
			fp.write(poa_utils.hcl_get_file_contents(self.ppa_runner, self.poa_host_id, remote_filename))

	def get_file_contents(self, remote_filename):
		return poa_utils.hcl_get_file_contents(self.ppa_runner, self.poa_host_id, remote_filename)

	def upload_file(self, local_filename, remote_filename):
		with open(local_filename, 'rb') as fp:
			return poa_utils.hcl_upload(
				self.ppa_runner, self.poa_host_id,
				remote_filename, fp.read()
			)

	def upload_file_content(self, filename, content):
		return poa_utils.hcl_upload(self.ppa_runner, self.poa_host_id, filename, content)

	def remove_file(self, filename):
		return self.run('/bin/rm', [filename])

	def mkdir(self, dirname):
		return self.run('/bin/mkdir', ['-p', dirname])

	def get_files_list(self, path):
		return unix_utils.get_files_list(self, path)

	def _get_host_description(self):
		return self.host_description if self.host_description is not None else "PPA service node #%s" % (self.poa_host_id,)

	@staticmethod
	def _get_cmd_str_listargs(cmd, args):
		return unix_utils.format_command_list(cmd, args)

	def _get_cmd_str_formatargs(self, cmd_str, args):
		run_cmd, run_args = self._get_command_list(cmd_str, args)
		return unix_utils.format_command_list(run_cmd, run_args)

def hcl_execute_multiple_attempts(run_function, host, command, max_attempts=5, interval_between_attempts=10):
	for attempt in range(0, max_attempts):
		try:
			result = run_function()
			if attempt > 0:
				logger.info(
					'Command "{command}" was executed successfully at {host} after {attempts} attempt(s).'.format(
						command=command, host=host, attempts=attempt+1
					)
				)
			return result
		except HclRunnerException as e:
			logger.debug("Exception: ", exc_info=True)
			if attempt >= max_attempts - 1:
				raise e
			else:
				logger.error(
					'Failed to execute remote command, retry in {interval_between_attempts} seconds'.format(
						interval_between_attempts=interval_between_attempts
					)
				)
				sleep(interval_between_attempts, 'Retry executing command')

def hcl_is_windows(ppa_runner, host_id, host_description=None):
	"""Hack to detect service node platform"""

	host_description = host_description if host_description is not None else "PPA service node #%s" % (host_id,)
	windows_pattern = "User '' does not exist"
	command = 'cmd /c REM'

	def run_func():
		# REM - command used for comments in batch file; does nothing, has no output.
		try:
			exit_code, stdout, stderr = poa_utils.windows_exec_unchecked(ppa_runner, host_id, command, 'utf-8')
			if exit_code != 0: # looks like Windows, but there are some problems executing the command, so raise HclRunnerException which means - try again
				raise HclRunnerException(strip_multiline_spaces("""
					Failed to detect OS of the node. The command "{command}" executed at {host_description} returned a non-zero exit code. 
					============================================================================
					Stderr:\n{stderr}
					============================================================================
					Stdout:\n{stdout}
					============================================================================
					1. If this happened because of a network issue, re-run the migration tool.
					2. To investigate the issue, execute an HCL request and look at the result:
					2.1. Log in to the PPA management node.
					2.2. Create an XML file with an HCL request (/tmp/tmp-hcl for example):
					{hcl_script}
					2.3. Run the command "/usr/local/pem/bin/pleskd_ctl -f /usr/local/pem/etc/pleskd.props processHCL /tmp/tmp-hcl {host_id}"
					2.4. If node OS is Windows, expect error message '{windows_pattern}', otherwise expect no error messages
					""").format(
						stderr=stderr,
						stdout=stdout,
						command=command,
						host_id=host_id,
						host_description=host_description,
						hcl_script=poa_utils.get_windows_hcl_script(command),
						windows_pattern=windows_pattern
					),
					stdout, stderr,
					host_description=host_description,
				)
			return True # no errors occured, so that is Windows
		except poa_utils.HclPleskdCtlNonZeroExitCodeError as e:
			if windows_pattern in e.stdout or windows_pattern in e.stderr:
				return False
			else:
				# looks like a problem communicating the node, or something else unexpected - try again
				raise HclRunnerException(strip_multiline_spaces("""
					Failed to detect OS of the node. The command "{command}" executed at {host_description} returned a non-zero exit code.
					============================================================================
					Stderr:\n{stderr}
					============================================================================
					Stdout:\n{stdout}
					============================================================================
					1. If this happened because of a network issue, re-run the migration tool.
					2. To investigate the issue, execute an HCL request and look at the result:
					2.1. Log in to the PPA management node.
					2.2. Create an XML file with an HCL request (/tmp/tmp-hcl for example):
					{hcl_script}
					2.3. Run the command "/usr/local/pem/bin/pleskd_ctl -f /usr/local/pem/etc/pleskd.props processHCL /tmp/tmp-hcl {host_id}"
					2.4. If node OS is Windows, expect error message '{windows_pattern}', otherwise expect no error messages
					""").format(
						stderr=e.stderr,
						stdout=e.stdout,
						command=command,
						host_id=host_id,
						host_description=host_description,
						hcl_script=poa_utils.get_windows_hcl_script(command),
						windows_pattern=windows_pattern
					),
					e.stdout, e.stderr,
					host_description=host_description,
					cause=e
				)

	return hcl_execute_multiple_attempts(run_func, host_description, command)

class SSHRunner(BaseRunner):
	def __init__(self, ssh, host_description=None):
		self.ssh = ssh
		self.host_description = host_description

	@property
	def codepage(self):
		return 'utf-8'

	def run(
			self, cmd, args=None, stdin_content=None, output_codepage=None,
			error_policy='strict', env=None):
		"""The same as run_unchecked(), but checks exit code and returns only stdout"""
		exit_code, stdout, stderr = self.run_unchecked(
				cmd, args, stdin_content, output_codepage, error_policy, env)
		if exit_code != 0:
			self._raise_runner_exception(
				unix_utils.format_command_list(cmd, args), stdout, stderr)
		return stdout 
	
	def run_unchecked(
			self, cmd, args=None, stdin_content=None, codepage=None,
			error_policy='strict', env=None):
		"""Execute a command with a list of positional 'args'."""
		formatter = unix_utils.format_command_list
		return self._exec(
			cmd, args, formatter, stdin_content, codepage, error_policy, env)

	def sh_unchecked(
			self, cmd, args=None, stdin_content=None, output_codepage=None,
			error_policy='strict', env=None):
		"""Format and execute a command with a dict of named 'args'."""
		formatter = self._get_command_str
		return self._exec(
			cmd, args, formatter, stdin_content, output_codepage, error_policy, env)
		
	def _exec(self, cmd, args, formatter, stdin_content, output_codepage, error_policy, env):
		self._log_command(cmd, args, formatter)
		codepage = output_codepage if output_codepage else self.codepage
		command_str = formatter(cmd, args)
		with profiler.measure_command_call(
				command_str, self.host_description):
			return ssh_utils.run_unchecked(
					self.ssh, command_str, stdin_content, codepage,
					error_policy, env)

	def _log_command(self, cmd, args, formatter):
		"""Write command string to log while omitting sensitive data."""
		from parallels.common.utils.migrator_utils import get_bleeping_logger
		blogger = get_bleeping_logger(logger)
		if isinstance(args, dict):
			command = NamedArgCommand(cmd, args, formatter)
		else:
			command = PositionalArgCommand(cmd, args, formatter)
		blogger.debug(
			u'Execute remote command: ssh %s "%s"', self.ssh.hostname, command)

	def sh(
			self, cmd_str, args=None, stdin_content=None, output_codepage=None,
			error_policy='strict', env=None):
		"""The same as sh_unchecked(), but checks exit code and returns only stdout"""
		return self.sh_multiple_attempts(
				cmd_str, args, stdin_content, output_codepage, error_policy,
				env, max_attempts = 1)

	def upload_file(self, local_filename, remote_filename):
		with closing(self.ssh.open_sftp()) as sftp:
			sftp.put(local_filename, remote_filename)

	def get_file(self, remote_filename, local_filename):
		with closing(self.ssh.open_sftp()) as sftp:
			sftp.get(remote_filename, local_filename)

	def get_file_contents(self, remote_filename):
		return self.run('/bin/cat', [remote_filename,])

	def remove_file(self, filename):
		return self.run('/bin/rm', [filename])

	def mkdir(self, dirname):
		return self.run('/bin/mkdir', ['-p', dirname])

	def get_files_list(self, path):
		return unix_utils.get_files_list(self, path)

	def _raise_runner_exception(self, command, stdout, stderr):
		if self.host_description is not None:
			where = " executed at %s" % (self.host_description,)
		else:
			where = ""

		raise SSHRunnerException(
			u"""The command "{command}"{where} returned a non-zero exit code.\n"""
			u"============================================================================\n"
			u"Stderr:\n{stderr}\n"
			u"============================================================================\n"
			u"Stdout:\n{stdout}\n"
			u"============================================================================\n\n"
			u"1. If this happened because of a network issue, re-run the migration tool.\n"
			u"2. Ensure that the host is up and there are no firewall rules that may block SSH connection to the host.\n"
			u"3. To investigate the issue, login to the host by SSH, run the command and look at the result.\n".format(
				command=command, where=where, stderr=safe_string_repr(stderr), stdout=safe_string_repr(stdout)
			),
			command_str=command,
			stdout=stdout,
			stderr=stderr
		)

	@staticmethod
	def _get_command_str(cmd_str, args=None):
		"""Format command string with named formatting arguments."""
		if args is not None and len(args) > 0:
			cmd_str = unix_utils.format_command(cmd_str, **args)
		return cmd_str

	def file_exists(self, filename):
		return unix_utils.file_exists(self, filename)

class WindowsAgentRunner(BaseRunner):
	"""Runner for Windows nodes which have server based on windows-agent/server.py

	Runner tries to install Windows agent automatically on remote node with paexec.
	In case that is not possible (for example, there is no Samba connection, so
	paexec does not work), you could install agent on remote node manually.
	"""
	def __init__(self, settings, migrator_server, host_description=None):
		self.remote = WindowsAgentPool.get_instance(migrator_server).get(settings)
		self.settings = settings
		if host_description is not None:
			self.host_description = settings.ip
		else:
			self.host_description = host_description
		self.host_description = settings.ip

	def check(self, server_description):
		pass

	def sh_unchecked(self, cmd_str, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		if args is not None:
			cmd_str_formatted = windows_utils.format_command(cmd_str, **args)
		else:
			cmd_str_formatted = cmd_str
		logger.debug("Execute command at '%s': %s", self.settings.ip, cmd_str_formatted)
		with profiler.measure_command_call(
			cmd_str_formatted, self.host_description
		):
			exit_code, stdout, stderr = self.remote.sh_unchecked(cmd_str, args, stdin_content)

		logger.debug(u"Command stdout: %s" % stdout)
		logger.debug(u"Command stderr: %s" % stderr)
		logger.debug(u"Command exit code: %s" % exit_code)

		return exit_code, stdout, stderr

	def run_unchecked(
			self, cmd, args=None, stdin_content=None, output_codepage=None,
			error_policy='strict', env=None):
		command = windows_utils.format_command_list(cmd, args)
		args = None
		return self.sh_unchecked(
			command, args, stdin_content, output_codepage, error_policy, env)

	def get_file(self, remote_filename, local_filename):
		logger.debug(
			"Download file '%s' to '%s' from '%s",
			remote_filename, local_filename, self.settings.ip
		)
		with open(local_filename, 'wb') as fp:
			fp.write(self.get_file_contents(remote_filename))

	def upload_file(self, local_filename, remote_filename):
		logger.debug(
			"Upload file '%s' to '%s' at '%s",
			local_filename, remote_filename, self.settings.ip
		)
		with open(local_filename, 'rb') as fp:
			self.upload_file_content(remote_filename, fp.read())

	def upload_file_content(self, filename, content):
		logger.debug(
			"Upload file contents to '%s' at '%s",
			filename, self.settings.ip
		)
		with profiler.measure_command_call(
			"Upload file '%s'" % (filename,),
			self.host_description
		):
			self.remote.upload_file_content(filename, content)

	def get_file_contents(self, remote_filename):
		logger.debug(
			"Download contents of '%s' file from '%s",
			remote_filename, self.settings.ip
		)
		with profiler.measure_command_call(
			"Download file '%s'" % (remote_filename,),
			self.host_description
		):
			return self.remote.get_file_contents(remote_filename)

	def remove_file(self, filename):
		logger.debug(
			"Remove file '%s' from '%s'",
			filename, self.settings.ip
		)
		with profiler.measure_command_call(
			"Remove file '%s'" % (filename),
			self.host_description
		):
			self.remote.remove_file(filename)

	def mkdir(self, dirname):
		logger.debug(
			"Create directory '%s' at '%s'",
			dirname, self.settings.ip
		)
		with profiler.measure_command_call(
			"Create directory '%s'" % (dirname,),
			self.host_description
		):
			self.remote.mkdir(dirname)

	@staticmethod
	def _get_command_str(cmd_str, args=None):
		return windows_utils.format_command(cmd_str, **args)

	def _raise_runner_exception(self, cmd, stdout, stderr):
		raise BaseRunnerException(
			"Failed to execute command '%s'. Stdout: %s. Stderr: %s." % (
				cmd, stdout, stderr))

	def file_exists(self, filename):
		return self.remote.file_exists(filename)

class PAExecRunner(BaseRunner):
	"""Execute commands on remote Windows server with paexec utility"""

	def __init__(self, settings, migrator_server, source_server):
		self.global_context = Registry.get_instance().get_context()
		self.settings = settings
		self.migrator_server = migrator_server
		self.source_server = source_server

	def sh_unchecked(self, cmd_str, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		logger.debug("Execute command on remote server with paexec")
		if stdin_content is not None:
			raise NotImplementedError(u"This runner does not support stdin_content parameter")
		if env is not None:
			raise NotImplementedError(u"The parameter 'env' is not supported")

		if args is not None:
			cmd_str = windows_utils.format_command(cmd_str, **args)

		with profiler.measure_command_call(
			cmd_str, self.settings.ip
		):
			passwords_file = self.migrator_server.get_session_file_path(
				'node-password-%s' % self.settings.ip
			)
			with open(passwords_file, 'wb') as f:
				f.write(self.settings.windows_auth.password)
			return LocalWindowsRunner.get_instance(disable_profiling=True).sh_unchecked(
				r'%s \\%s -u %s -p@ %s -p@d ' % (
					windows_thirdparty.get_paexec_bin(),
					self.settings.ip,
					self.settings.windows_auth.username,
					passwords_file	
				) + cmd_str
			)

	def run_unchecked(
			self, cmd, args=None, stdin_content=None, output_codepage=None,
			error_policy='strict', env=None):
		command = windows_utils.format_command_list(cmd, args)
		args = None
		return self.sh_unchecked(
				command, args, stdin_content, output_codepage, error_policy, env)

	def get_file(self, remote_filename, local_filename):
		rsync = self.global_context.rsync_pool.get(self.source_server, self.migrator_server)
		self.sh(r'cmd /c copy "%s" c:\migrator\download' % remote_filename)
		rsync.sync(
			source_path='migrator/download',
			target_path=windows_utils.convert_path_to_cygwin(local_filename)
		)
		self.remove_file(r'c:\migrator\download')

	def get_file_contents(self, remote_filename):
		temp_filename = self.migrator_server.get_session_file_path('paexec_download')
		try:
			self.get_file(remote_filename, temp_filename)
			with open(temp_filename, 'rb') as f:
				return f.read()
		finally:
			if os.path.isfile(temp_filename):
				os.remove(temp_filename)

	def upload_file_content(self, filename, content):
		local_temp_file = self.migrator_server.get_session_file_path('upload')
		with open(local_temp_file, "wb") as fp:
			fp.write(content)
		self.upload_file(local_temp_file, filename)

	def upload_file(self, local_filename, remote_filename):
		rsync = self.global_context.rsync_pool.get(self.source_server, self.migrator_server)
		rsync.upload(
			source_path=windows_utils.convert_path_to_cygwin(local_filename),
			target_path='migrator/upload'
		)
		self.sh(r'cmd /c move c:\migrator\upload "%s"' % remote_filename)

	def mkdir(self, dirname):
		self.sh('cmd /c if not exist {dirname} ( md {dirname} )', dict(dirname=dirname))

	def remove_file(self, filename):
		self.sh('cmd /c del {filename}', dict(filename=filename))

	def remove_directory(self, directory, is_remove_root=True):
		# remove all files in a directory recursively
		self.sh(
			'cmd /c del {directory} /s /q',
			dict(directory=directory)
		)
		# remove all directories recursively
		directories_to_delete = directory if is_remove_root else ntpath.join(directory, '*')
		self.sh(
			u'cmd /c for /d %p in ({directories_to_delete}) do rmdir "%p" /s /q',
			dict(directories_to_delete=directories_to_delete)
		)

	def check(self, server_description):
		try:
			self.sh('cmd /c echo 1')
		except Exception:
			logger.debug('Exception: ', exc_info=True)
			raise ConnectionCheckError(
				"Failed to connect to server %s with paexec utility\n"
				"Check that:\n"
				"1) Correct Windows login and password are specified in config.ini.\n"
				"2) Samba service is working on the Windows server." % (
					server_description
				)
			)

class WinexeRunner(WindowsRunner):
	"""Remotely execute Windows CMD commands on Linux using 'winexe' tool."""
	def __init__(self, settings, runas = False, local_temp_dir = None):
		self.settings = settings
		self.runas = runas
		self.local_temp_dir = local_temp_dir

	def run_unchecked(self, cmd, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		if stdin_content is not None:
			raise NotImplementedError(u"This runner does not support stdin_content parameter")

		command_str = windows_utils.format_command_list(cmd, args)
		with profiler.measure_command_call(
			command_str, self.settings.ip
		):
			return self._run_command_and_decode_output(command_str, output_codepage, error_policy, env)

	def sh_unchecked(self, cmd_str, args=None, stdin_content=None, output_codepage=None, error_policy='strict', env=None):
		if stdin_content is not None:
			raise NotImplementedError(u"This runner does not support stdin_content parameter")

		if args is not None:
			cmd_str = windows_utils.format_command(cmd_str, **args)

		with profiler.measure_command_call(
			cmd_str, self.settings.ip
		):
			return self._run_command_and_decode_output(cmd_str, output_codepage, error_policy, env)

	def run(
			self, cmd, args=None, stdin_content=None, output_codepage=None,
			error_policy='strict', env=None):
		"""The same as run_unchecked(), but checks exit code and returns only stdout"""
		exit_code, stdout, stderr = self.run_unchecked(
				cmd, args, stdin_content, output_codepage, error_policy, env)
		if exit_code != 0:
			self._raise_runner_exception(
				windows_utils.format_command_list(cmd, args), stdout, stderr)
		return stdout

	def sh(
			self, cmd_str, args=None, stdin_content=None, output_codepage=None,
			error_policy='strict', env=None):
		"""The same as sh_unchecked(), but checks exit code and returns only stdout"""
		return self.sh_multiple_attempts(
				cmd_str, args, stdin_content, output_codepage, error_policy,
				env, max_attempts=1)

	def _run_command_and_decode_output(self, cmd_str, output_codepage=None, error_policy='strict', env=None):
		"""Run a command, return its output decoded from 'codepage' to UTF."""
		if env is not None:
			raise NotImplementedError(u"The parameter 'env' is not supported")
		check_windows_national_symbols_command(cmd_str)
		formatted_command = self._construct_winexe_command(cmd_str)
		codepage = output_codepage if output_codepage else self.codepage

		errors = []
		def run_winexe_command():
			exit_code, stdout, stderr = local_command_exec(formatted_command, None, codepage, error_policy, env)

			# remove winexe warning with no impact on us
			stderr = stderr.replace(
				"dos charset 'CP850' unavailable - using ASCII\n", ''
			)

			if stderr.find(u"Failed to open connection - NT_STATUS_LOGON_FAILURE") != -1:
				errors.append(u"Command '%s' failed to execute with '%s' error. Please check that credentials for server '%s' are correct in config.ini" % (
					u" ".join([pipes.quote(c) for c in formatted_command]),
					stderr.strip(),
					self.settings.ip
				))
				return None
			elif stderr.find(u"Failed to open connection") != -1:
				errors.append(u"Command '%s' failed to execute with '%s' error. Please check that connection settings for server '%s' are correct in config.ini" % (
					u" ".join([pipes.quote(c) for c in formatted_command]),
					stderr.strip(),
					self.settings.ip
				))
				return None
			elif stderr.find(u"NT_STATUS_") != -1:
				errors.append(u"Command '%s' failed to execute with '%s' error" % (
					u" ".join([pipes.quote(c) for c in formatted_command]),
					stderr.strip()
				))
				return None
			else:
				return exit_code, stdout, stderr 
		
		result = poll_data(run_winexe_command, polling_intervals(starting=3, max_attempt_interval=5, max_total_time=30))
		if result is not None:
			return result
		else:
			logger.debug(u"Errors while executing command:\n%s" % "\n".join(errors))
			raise MigrationError(errors[len(errors)-1])

	def upload_file(self, local_filename, remote_filename):
		# implementation note: winexe runner utilizes smbclient to upload files
		windows_utils.upload_file(local_filename, remote_filename, self.settings)

	def upload_file_content(self, filename, content):
		if self.local_temp_dir is None:
			raise Exception("Unable to upload file contents to %s: directory for temporary files was not specified to WinexeRunner" % (filename,))
		# implementation note: winexe runner utilizes smbclient to upload files
		temp_filename = os.path.join(self.local_temp_dir, 'winexe_upload')	
		try:
			with open(temp_filename, "w") as f:
				f.write(content)

			windows_utils.upload_file(temp_filename, filename, self.settings)
		finally:
			if os.path.isfile(temp_filename):
				os.remove(temp_filename)

	def get_file(self, remote_filename, local_filename):
		# implementation note: winexe runner utilizes smbclient to download files
		windows_utils.download_file(remote_filename, local_filename, self.settings)

	def get_file_contents(self, remote_filename):
		if self.local_temp_dir is None:
			raise Exception(
				"Unable to download file contents to %s: "
				"directory for temporary files was not "
				"specified to WinexeRunner" % (remote_filename,)
			)
		temp_filename = os.path.join(self.local_temp_dir, 'winexe_download')
		try:
			windows_utils.download_file(
				remote_filename, temp_filename, self.settings
			)
			with open(temp_filename, 'rb') as f:
				return f.read()
		finally:
			if os.path.isfile(temp_filename):
				os.remove(temp_filename)

	def mkdir(self, dirname):
		self.sh('cmd /c if not exist {dirname} ( md {dirname} )', dict(dirname=dirname))

	def check(self, server_description):
		try:
			logger.debug(u"Try to connect to samba share C$ on %s." % server_description)
			windows_utils.run_smbclient_cmd(
				 # no command - just connect to the share and exit
				"", "C$", self.settings
			)
		except Exception as err:
			logger.debug(u"Exception:", exc_info=err)
			raise ConnectionCheckError(strip_multiline_spaces(
			u"""Failed to connect to samba share C$ on server '%s': %s. 
				Check that:
				1) Correct Windows login and password are specified in config.ini.
				2) Samba service is working on the Windows server.
				""" % (server_description, err)
			))

	def _construct_winexe_command(self, cmd_str):
		formated_command = [
			u"winexe", u"--debug-stderr", u"--reinstall", u"-U",
			"%s%%%s" % (
				self.settings.windows_auth.username,
				self.settings.windows_auth.password),
			u"//%s" % (self.settings.ip,),
			cmd_str
		]

		if self.runas:
			formated_command += [
				u"--runas", "%s%%%s" % (
					self.settings.windows_auth.username, self.settings.windows_auth.password)]

		return formated_command

	@staticmethod
	def _get_cmd_str_listargs(cmd, args):
		return " ".join([windows_utils.quote_arg(arg) for arg in [cmd] + args])

	@staticmethod
	def _get_cmd_str_formatargs(cmd_str, args):
		if args is not None:
			return windows_utils.format_command(cmd_str, **args)
		else:
			return cmd_str

	def _raise_runner_exception(self, command_str, stdout, stderr):
		full_command = self._construct_winexe_command(command_str)
		full_cmd = full_command[0]
		full_args = full_command[1:]
		full_command_str = unix_utils.format_command_list(full_cmd, full_args)
		where = " executed at %s" % (self.settings.ip,)
		raise WinexeRunnerException(strip_multiline_spaces("""
				The command "{command_str}"{where} returned a non-zero exit code.
				============================================================================
				Stderr:\n{stderr}
				============================================================================
				Stdout:\n{stdout}
				============================================================================
				1. If this happened because of a network issue, re-run the migration tool.
				2. Ensure that the host is up and there are no firewall rules that may block winexe connection to the host.
				3. To investigate the issue:
				- login to the host and run the command:
				{command_str}
				- try to run the command with winexe from the current host:
				{full_command_str}
			""").format(
				command_str=command_str, full_command_str=full_command_str,
				where=where, stderr=safe_string_repr(stderr), stdout=safe_string_repr(stdout)
			), 
			command_str=command_str, full_command_str=full_command_str,
			stderr=stderr, stdout=stdout
		)

	@staticmethod
	def _get_command_str(cmd_str, args=None):
		if args is not None:
			cmd_str = windows_utils.format_command(cmd_str, **args)
		return cmd_str
