from contextlib import contextmanager
import struct
import subprocess
import pickle
import ssl
import logging
import logging.config
import zipfile
from StringIO import StringIO
import yaml
import sys
import os
import shutil
import errno
import SocketServer
import ConfigParser

try:
	with open('logging.config') as f:
		logging.config.dictConfig(yaml.load(f))
	logging.captureWarnings(True)
except Exception as e:
	print ("Failed to load logging configuration: %s. No logging will be performed." % (str(e)))

logger = logging.getLogger('parallels')
logger.info("Start Parallels Panel Transfer agent")

logger.info("Read configuration")
config_parser = ConfigParser.ConfigParser()
config_filename = 'config.ini'


@contextmanager
def log_failure(message):
	try:
		yield
	except Exception:
		logger.error(message)
		logger.debug("Exception: ", exc_info=True)
		raise


class ServerConfiguration(object):
	def __init__(self):
		self.use_ssl = False
		self.client_cert = None
		self.server_cert = None
		self.server_key = None
		self.port = None

	def __repr__(self):
		return (
			"ServerConfiguration("
			"use_ssl={use_ssl}, "
			"client_cert={client_cert}, "
			"server_cert={server_cert}, "
			"server_key={server_key}, "
			"port={port}"
			")".format(
				use_ssl=self.use_ssl, 
				client_cert=self.client_cert, 
				server_cert=self.server_cert, 
				server_key=self.server_key, 
				port=self.port
				)
		)

try:
	config_parser.read(config_filename)
	configuration = ServerConfiguration()
	configuration.use_ssl = config_parser.getboolean('GLOBAL', 'use-ssl')
	configuration.client_cert = config_parser.get('GLOBAL', 'client-cert')
	configuration.server_cert = config_parser.get('GLOBAL', 'server-cert')
	configuration.server_key = config_parser.get('GLOBAL', 'server-key')
	configuration.port = config_parser.getint('GLOBAL', 'port')
except Exception as e:
	logger.debug(u'Exception:', exc_info=e)
	logger.info("Failed to read configuration file '%s': %s", config_filename, e)
	sys.exit(1)

logger.debug("Server configuration: %r", configuration)


class LocalRunner(object):
	def sh_unchecked(self, cmd_str, args=None, stdin_content=None, env=None):
		if args is not None:
			cmd_str = self._format_command(cmd_str, **args)
		logger.debug("Execute command: %s", cmd_str)

		p = subprocess.Popen(cmd_str, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
		stdout_str, stderr_str = [s.decode('utf-8', 'strict') for s in p.communicate(stdin_content)]
		logger.debug(u"Command stdout: %s" % stdout_str)
		logger.debug(u"Command stderr: %s" % stderr_str)
		logger.debug(u"Command exit code: %s" % p.returncode)

		return p.returncode, stdout_str, stderr_str

	@staticmethod
	def get_file_contents(remote_filename):
		logger.debug("Get contents of file '%s'", remote_filename)

		with open(remote_filename, 'rb') as fp:
			return fp.read()

	@staticmethod
	def upload_file_content(filename, contents):
		logger.debug("Put contents to file '%s'", filename)

		with open(filename, "wb") as fp:
			fp.write(contents)

	@staticmethod
	def mkdir(dirname):
		logger.debug("Create directory '%s'", dirname)

		try:
			os.makedirs(dirname)
		except OSError as exc:
			if exc.errno == errno.EEXIST:
				pass
			else:
				raise

	@staticmethod
	def move(src_path, dst_path):
		logger.debug("Move '%s' to '%s'" % (src_path, dst_path))
		shutil.move(src_path, dst_path)

	@staticmethod
	def get_files_list(path):
		logger.debug("List '%s'" % path)
		return os.listdir(path)

	@staticmethod
	def get_directories_list_recursive(path):
		logger.debug("List directories at '%s'" % path)
		directories = []
		for root, dirs, _ in os.walk(path):
			for directory in dirs:
				directories.append(os.path.relpath(os.path.join(root, directory), path))
		return directories

	@staticmethod
	def get_files_list_recursive(path):
		logger.debug("List files at '%s'" % path)
		files_list = []
		for root, _, files in os.walk(path):
			for filename in files:
				files_list.append(os.path.relpath(os.path.join(root, filename), path))
		return files_list

	@staticmethod
	def get_directory_as_zip(path):
		archive = StringIO()
		with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED) as zfp:
			for root, dirs, files in os.walk(path):
				for filename in files:
					zfp.write(os.path.join(root, filename), os.path.relpath(os.path.join(root, filename), path))
		return archive.getvalue()

	@staticmethod
	def remove_file(filename):
		logger.debug("Remove file '%s'", filename)

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

	@staticmethod
	def file_exists(filename):
		logger.debug("Check if file '%s' already exists", filename)
		return os.path.exists(filename)

	@staticmethod
	def callback(command, ip, login, password):
		with open('.pwd', 'w') as password_file:
			password_file.write(u'%s' % password)
		paexec = ur'paexec.exe \\{ip} -u {login} -p@ .pwd -p@d -d'.format(ip=ip, login=login, password=password)
		try:
			local_command = ur'cmd.exe /c "{paexec} cmd.exe /c "{command}""'.format(
				paexec=paexec, command=command
			)
			logger.debug("Local command to initiate callback: '%s'" % local_command)
			p = subprocess.Popen(local_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
			stdout, stderr = [s.decode('utf-8', 'strict') for s in p.communicate()]
			logger.debug("Command '{command}' executed at {ip}\nstdout:\n{stdout}\nstderr:\n{stderr}".format(
				command=command, ip=ip, stdout=stdout, stderr=stderr
			))
			logger.info("Command '%s' executed successfully at %s", command, ip)
		except Exception as exception:
			logger.debug(u'Exception:', exc_info=exception)
			logger.info("Failed to execute command '%s' at %s", command, ip)
			return False

		return True

	def _format_command(self, template, *args, **kw):
		qargs = map(self._quote_arg, args)
		qkw = dict((k, self._quote_arg(unicode(v))) for k, v in kw.iteritems())
		return unicode(template).format(*qargs, **qkw)

	@staticmethod
	def _quote_arg(arg):
		return '"%s"' % arg.replace('"', '""')


class ConnectionHandler(SocketServer.BaseRequestHandler):
	def handle(self):
		client_address = self.client_address[0]
		logger.info("Connected by '%s'", client_address)
		runner = LocalRunner()
		try:
			while True:
				with log_failure("Failed to receive length of a command"):
					length = self._receive(4)
					if not length:
						break
					length, = struct.unpack("I", length)

				with log_failure("Failed to receive command"):
					command = self._receive(length)
					if not command:
						break

				with log_failure("Failed to unserialize command"):
					function, args, kwargs = pickle.loads(command)

				try:
					execution_result = getattr(runner, function)(*args, **kwargs)
				except:
					logger.error("Failed to execute received command for function '%s'" % function)
					logger.debug("Exception: ", exc_info=True)
					self.request.sendall(struct.pack("I", 0))
				else:
					with log_failure("Failed to serialize command execution results"):
						serialized_result = pickle.dumps(execution_result)

					with log_failure("Failed to send execution results back"):
						self.request.sendall(struct.pack("I", len(serialized_result)))
						self.request.sendall(serialized_result)
		finally:
			logger.info("Finished connection from '%s'", client_address)

	def _receive(self, size):
		b = ''
		while len(b) < size:
			r = self.request.recv(size - len(b))
			if not r:
				return b
			b += r
		return b


class AgentServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
	def __init__(self, configuration):
		SocketServer.TCPServer.__init__(self, ('', configuration.port), ConnectionHandler, bind_and_activate=False)
		self.configuration = configuration
		if self.configuration.use_ssl:
			self.socket = ssl.wrap_socket(
				self.socket, server_side=True, 
				keyfile=self.configuration.server_key,
				certfile=self.configuration.server_cert, 
				cert_reqs=ssl.CERT_REQUIRED, 
				ca_certs=configuration.client_cert
			)

		self.server_bind()
		self.server_activate()

server = AgentServer(configuration)
server.serve_forever()
