#!/usr/bin/python

# Sshlink - Create a SSH backlink to connect to a host behind a firewall
#
# Copyright (C) 2006-2007 Arnau Sanchez
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License or any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation, 
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

# Standard python modules
import os, sys, signal, optparse
import time, socket, popen2
import threading

# Private modules
package = "sshlink"
sys.path.append("/usr/lib/%s"%package)
import sshclient, daemonize, cfgparser

# Global variables
debug = False
default_conffile = "/etc/%s.conf"%package
default_pidfile = "/var/run/%s.pid"%package
default_ssh_port = 22
options_definition = \
	dict(name="ssh_command"), \
	dict(name="ssh_args"), \
	dict(name="log_time", type="integer", default=3600), \
	dict(name="checkalive_time", type="integer", default=30), \
	dict(name="password"), \
	dict(name="passphrase"), \
	dict(name="remote_logging", default="daemon.info"), \
	dict(name="run_remote_at_connection"), \
	dict(name="run_remote_at_checkalive"), \
	dict(name="run_local_at_connection"), \
	dict(name="run_local_at_checkalive"), \
	dict(name="connect", type="boolean")
	
#######################################################
class SSHclientThread(threading.Thread):
	_test_command = "/usr/bin/test"
	_log_command = "/usr/bin/logger -t %s"%package
	##################################
	def __init__(self, name, options, test_command=_test_command, debug=False, test=False):
		threading.Thread.__init__(self)
		self.name = name
		self.options = options
		self.test_command = test_command
		self.debug = debug
		self.test = test
		self.hostname = socket.gethostname()
	
	#######################################
	def pdebug(self, line):
		if not self.debug: return
		sys.stderr.write(self.name+": "+line+"\n")
		sys.stderr.flush()

	#######################################
	def run_remote(self, command, loginfo):
		if not command: 
			return 0
		try: 
			self.client.command(command)
		except: 
			self.pdebug("%s error: %s" %(loginfo, command))
			return 1
		return 0

	###############################
	def run_local(self, command, loginfo, input=None):
		if not command: 
			return
		self.pdebug("%s: %s" %(loginfo, command))
		try: 
			popen = popen2.Popen3(command)
		except: 
			self.pdebug("%s error: %s" %(loginfo, command))
			return 1
		if input: 
			popen.tochild.write(input)
		popen.tochild.close()
		output = popen.fromchild.read()
		popen.fromchild.close()
		rv = popen.wait() >> 8
		self.pdebug("run_local: retcode=%d" %rv)
		return rv
		
	##################################
	def run(self):
		opts = self.options
		for opt in "ssh_command", "ssh_args":
			if opt not in opts: 
				error("required '%s' not defined"%opt)
				return
		command = " ".join([opts["ssh_command"], opts["ssh_args"]])
		while 1:
			self.connect(command)
			if self.test: 
				self.pdebug("test mode enabled, break infinite loop")
				break
			self.pdebug("connection lost")
			time.sleep(1)
	
	#########################################
	def connect(self, command):
		self.client = sshclient.SSHClient(debug=debug)
		opts = self.options
		try: 
			self.client.connect(command, passphrase=opts["passphrase"], \
				password=opts["password"])
		except sshclient.SSHClientError, e: 
			self.pdebug("connection error: %s" %e)
			return
		command = '%s "%s start"' %(self._log_command, self.hostname)
		if self.run_remote(command, "logging command"): 
			return
		if self.run_remote(self.options["run_remote_at_connection"], "run_remote_at_connection command"): 
			return
		if self.run_local(self.options["run_local_at_connection"], "run_local_at_connection command"): 
			return
		log_time, checkalive_time = self.options["log_time"], self.options["checkalive_time"]
		next_log_time = time.time() + log_time
		if self.test: 
			return
		while 1:
			if self.run_remote(self.test_command, "test_link"): 
				break
			self.run_remote(self.options["run_remote_at_checkalive"], "run_remote_at_checkalive command")
			self.run_local(self.options["run_local_at_checkalive"], "run_local_at_checkalive command")
			if log_time and self.options["remote_logging"] and time.time() > next_log_time:
				self.pdebug("log_time: write log information")
				command = '%s -p %s "%s link-up"' %(self._log_command, self.options["remote_logging"], self.hostname)
				self.run_remote(command, "link_up logging")
				next_log_time = time.time() + log_time
			self.pdebug("sleep for %s seconds" %(checkalive_time))
			time.sleep(checkalive_time)

#######################################
def error(line):
	sys.stderr.write(line+"\n")
	sys.stderr.flush()
	
#######################################
def pdebug(line):
	error(line)

#######################################################
#######################################################
def main():	
	global debug	
	
	usage = """usage: %s [options] 

Create configurable SSH links""" %package
	parser = optparse.OptionParser(usage)
	parser.add_option('-b', '--background', dest='background', default=False, action='store_true', help='Run in background as daemon')
	parser.add_option('-d', '--debug', dest='debug', default=False, action='store_true', help='Enable debug mode')
	parser.add_option('-f', '--configuration-file', dest='conffile', default=default_conffile, metavar='FILE', type='string', help='Alternative configuration file')
	parser.add_option('-p', '--pidfile', dest='pidfile', default=default_pidfile, metavar='FILE', type='string', help='Alternative pidfile')
	parser.add_option('-t', '--test-host', dest='host', default="", metavar='NAME', type='string', help='Test a host connection and exit')
	poptions, args = parser.parse_args()
	
	debug = poptions.debug

	if poptions.background:
		if debug: pdebug("daemonizing...")
		daemonize.daemonize(poptions.pidfile)

	clients = []
	read_sections = []
	test_host = poptions.host
	if test_host: read_sections = [test_host]
	elif args: read_sections = args
	cparser = cfgparser.CfgParser(debug=debug)
	config = cparser.read_configuration(options_definition, poptions.conffile, sections=read_sections)
	if not config: 
		error("error reading configuration file: %s"%poptions.conffile)
		sys.exit(1)
	
	for host, options in config.items():
		if not options["connect"] and not test_host and host not in read_sections: 
			if debug: pdebug("host connection disabled in configuration: %s" %host)
			continue
		elif debug: pdebug("starting connection: %s" %host)
		tclient = SSHclientThread(host, options, test=bool(test_host), debug=debug)
		tclient.setDaemon(True)
		tclient.start()
		clients.append(tclient)
	
	if clients:
		if debug: pdebug("waiting threads to finish")
		while clients:
			for client in clients[:]:
				if not client.isAlive():
					clients.remove(client)
			time.sleep(0.5)
		if debug: pdebug("all threads finished")
	else: error("no thread was started")
	sys.exit(0)
		
#########
#############
#########################
if __name__ == '__main__':
        main()
