#!/usr/bin/python3
#-*- coding: utf-8 -*-
##############################################
# Home	: http://netkiller.github.io
# Author: Neo <netkiller@msn.com>
# Upgrade: 2020-04-29
##############################################
try:
	import os, io, sys, threading, time, getpass
	import logging, logging.handlers, configparser
	from optparse import OptionParser, OptionGroup
	from datetime import datetime
	# sys.path.append(basedir + '/lib/python3.6/site-packages')
	from netkiller.rsync import *
	from netkiller.git import *

except ImportError as err:
	print("%s" %(err))

basedir=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

CONFIG_DIR= basedir + '/etc'
#PROJECT_DIR = os.path.expanduser('~/project')
##############################################
class Deployment():
	debug = False
	def __init__(self,stage,domain):
		self.stage = stage
		self.domain = domain
		self.config = {}

		self.config		= self.configure(CONFIG_DIR+'/deployment.cfg','scm')
		self.project	= self.configure(CONFIG_DIR+'/deployment.cfg','project')
		
		if self.config['administrator'] != getpass.getuser():
			raise Exception("Current user "+os.getlogin()+" : Permission denied")
			sys.exit(127)
		
		filepath = os.path.expanduser(self.config['logdir'])
		if not os.path.exists(filepath):
			os.mkdir(filepath)
		self.logging = self.logfile(os.path.expanduser(self.config['logdir']+'/'+self.config['logfile']))

	def configure(self,inifile, section = None):
		conf = {}
		try:
			if not os.path.exists(inifile):
				raise Exception('Cannot open file', inifile)
			config = configparser.SafeConfigParser()
			config.read(inifile)
			if section :
				conf = dict(config.items(section))
			else:
				for sect in config.sections():
					conf[sect] = dict(config.items(sect))

		except configparser.NoSectionError as err:
			print("Error: %s %s" %(err, inifile))
			sys.exit(1)
		except Exception as err:
			print("Error: Cannot read ini file %s %s" %(err, inifile))
			sys.exit(1)
		if(self.debug):
			print('read ini file: '+inifile)
			print("%s %s" % (self.__class__.__name__, self.configure.__name__))
			print(conf)
		return(conf)
	def conf(self):
		conf = {}
		inifile = None

		self.logging.info('reload config '+ self.stage+' '+self.domain)
		host 		= self.domain[:self.domain.find('.')]
		inifile 	= os.path.expanduser(self.project['cfgdir']+'/'+self.stage+'/'+self.domain[self.domain.find('.')+1:]+'.ini')
		conf 		= self.configure(inifile, host)
		
		if ('source' not in conf):
			conf['source'] = self.project['source'] +'/'+ self.stage +'/'+ self.domain
		return(conf)
	def logfile(self,file):
		logger = None
		try:
			#logging.basicConfig(
			#	level=logging.NOTSET,
			#	format='%(asctime)s %(levelname)-8s %(message)s',
			#	datefmt='%Y-%m-%d %H:%M:%S',
			#	filename=file,
			#	filemode='a'
			#)
			logger = logging.getLogger()
			logger.setLevel(logging.DEBUG)
			handler = logging.handlers.TimedRotatingFileHandler(file, 'D', 1, 0)
			#handler.suffix = "%Y%m%d-%H%M.log"
			handler.suffix = "%Y-%m-%d.log"
			formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
			handler.setFormatter(formatter)
			
			logger.addHandler(handler)
		except AttributeError as err:
			print("Error: %s %s" %(err, file))
			sys.exit(2)
		except FileNotFoundError as err:
			print("%s" %(err))
			sys.exit(2)
		except PermissionError as err:
			print("Error: %s %s" %(err, file))
			sys.exit(2)
		return(logger)

	def buildconfig(self, source):
		try:
			inifile = os.path.expanduser(self.project['cfgdir']+'/'+self.stage+'/config/'+self.domain+'.ini')
			if os.path.exists(inifile):
				sections = self.configure(inifile)
				for filename in sections:
					cfgfile = source+'/'+filename
					self.logging.debug('build config file: '+cfgfile);
					if os.path.isfile(cfgfile) and os.access(cfgfile, os.R_OK):
						conf = self.configure(inifile, filename)
						with open(cfgfile+'.in','r', encoding='utf-8', errors='ignore') as f:
							lines = []
							for line in f.readlines():
								for key, val in conf.items():
									if line.find('{{'+key+'}}') :
										line = line.replace('{{'+key+'}}', val)
										self.logging.debug('replace config item: '+key+'=>'+val);
								lines.append(line)
						with open(cfgfile, 'w') as f:
							for line in lines:
								f.write(line)

		except IOError as err:
			print("Error: %s %s" %(err, cfgfile))
	def gulp(self, conf, source):
		if('gulp' in conf):
			gulpfile = ''
			stage = ''
			if ('gulp.path') in conf and (os.path.isdir(os.path.expanduser(conf['gulp.path']))) :
				os.chdir(os.path.expanduser(conf['gulp.path']))
			if 'gulp.gulpfile' in conf :
				gulpfile = '--gulpfile {}'.format(conf['gulp.gulpfile']);

			if(conf['gulp'] == 'all'):
					cmd = 'gulp {} --stage {} --src {}'.format(gulpfile, stage, source) 
			else:
					cmd = 'gulp {} {} --stage {} --src {}'.format(gulpfile, conf['gulp'], stage, source)
			
			screen = os.system(cmd)
			if self.debug :
				self.logging.debug( os.system('pwd') );
				self.logging.debug( cmd )
				self.logging.debug(screen)

	def yuicompressor(self,conf, source):
		if('yuicompressor' in conf):
			if(conf['yuicompressor'] == all):
				cmd = 'find '+source+' -type f -regex ".*\.\(css\|js\)" -exec yuicompressor {} -o {} \;';
			else:
				cmd = 'find '+source+' -type f -name "*.'+conf['yuicompressor']+'" -exec yuicompressor --type '+conf['yuicompressor']+' {} -o {} \;'
			screen = os.system(cmd)
			self.logging.debug(screen)
	def maven(self, conf, source):
		if ('maven' in conf):
			cmd = 'mvn {}'.format(conf['maven'])
			screen = os.system(cmd)
			self.logging.debug(screen)
			conf['dist']='target/*.?ar'
	def backup(self, options):
		conf = self.conf()

		if conf['mode'] == 'ssh':
			source = conf['remote']+':'+conf['destination']
		else:
			source = conf['remote']+'::'+conf['destination']
	
		backup = Rsync()
		backup.option('-auzv')
		backup.source(source)
		backup.destination(options.backup)
		backup.execute()
		if(self.debug):
			self.logging.debug(backup.string());
		self.logging.info('backup '+conf['remote']+'::'+conf['destination']+' -> '+options.backup);
	def build(self):
		build = os.path.expanduser(self.project['cfgdir']+'/'+self.stage+'/build/'+self.domain+'.sh')
		screen = os.system(build)
		if debug :
			self.logging.debug(build)
		self.logging.debug(screen)	
	def deploy(self, options):
		try:
			conf = self.conf()

			source = os.path.expanduser(conf['source'])

			if options.clean:
				import shutil
				shutil.rmtree(source)
				self.logging.warning('clean '+ source);
			if options.backup:
				self.backup(options)

			git = Git(source, self.logging)
			if os.path.isdir(source):
				if options.revert :
					revision = options.revert
					git.checkout(revision).branch().execute()
					self.logging.info('revision '+ revision);
				else:
					git.clean('-df').reset().pull().execute()
					self.logging.info('pull '+ conf['repository']);
			else:
				os.makedirs(source)
				git.clone(conf['repository']).execute()
				if 'branch' in conf:
					git.checkout(conf['branch']).execute()
					self.logging.info('checkout branch '+ conf['branch']);
				else:
					self.logging.info('clone '+ conf['repository']);

			if(self.debug):
				git.debug()

			if ('merge' in conf):
				self.merge(conf['branch'], conf['merge'])
			
			if ('build' in conf):
				self.build()
				
			self.buildconfig(source)
			
			if ('build' in conf):
				build = os.path.expanduser(self.project['cfgdir']+'/'+self.stage+'/build/'+self.domain+'.sh')
				screen = os.system(build)
				self.logging.debug(build)
				self.logging.debug(screen)

			self.yuicompressor(conf,source)
			self.gulp(conf,source)
			self.maven(conf,source)
			
			rsync = Rsync()

			if not 'mode' in conf:
				conf['mode'] = None
			
			option = '--exclude=.git --exclude=.svn --exclude=.gitignore'
			if options.trial:
				option = '-azvn ' + option + ' '
			else:
				option = '-azv ' + option + ' '

			if('option' in conf):
				rsync.option(option + conf['option'])
			else:
				rsync.option(option)

			if('delete' in conf):
				if conf['delete'].lower() in ("yes", "true", "y", "enable") :
					rsync.delete()

			if('logfile' in conf):
				if conf['logfile'] :
					rsync.logfile(conf['logfile'])
			else:
				logdir = os.path.expanduser(self.project['logdir']+'/'+self.stage)
				if not os.path.exists(logdir):
					os.makedirs(logdir)
				conf['logfile'] = logdir+'/'+self.domain+'.'+datetime.today().strftime('%Y-%m-%d.%H:%M:%S')+'.log'
				rsync.logfile(conf['logfile'])

			if('password' in conf):
				rsync.password(os.path.expanduser(conf['password']))

			if('backup' in conf):
				if conf['backup'] :
					conf['backup'] = conf['backup'] +'/'+self.stage+'/'+datetime.today().strftime('%Y-%m-%d.%H:%M:%S')
					rsync.backup(conf['backup'])
			else:
				backup_dir_prefix = ''
				if conf['mode'] == 'ssh':
					backup_dir_prefix = '~'
				conf['backup'] = backup_dir_prefix + self.project['backup']+'/'+self.stage+'/'+self.domain+'/'+datetime.today().strftime('%Y-%m-%d.%H:%M:%S')
				rsync.backup(conf['backup'])

			if('include' in conf):
				include = os.path.expanduser(self.project['cfgdir'] + '/include/' + conf['include'])
				if os.path.exists(include):
					rsync.include(include)
					#rsync.exclude(('"*"'))
				else:
					raise FileNotFoundError('Cannot open exclude file '+ include)

			if('exclude' in conf):
				exclude = os.path.expanduser(self.project['cfgdir'] + '/exclude/' + conf['exclude'])
				if os.path.exists(exclude):
					rsync.exclude(exclude)
				else:
					raise FileNotFoundError('Cannot open exclude file '+ exclude)
			
			if('dist' in conf):
				rsync.source(source+ '/'+ conf['dist'])
			else:
				rsync.source(source+'/')

			for rhost in conf['remote'].split(',') :
				print(">>> deploy to host %s" % (rhost))
				self.logging.debug('----- '+ rhost + ' -----');
				
				if conf['mode'] == 'ssh':
						rsync.destination(rhost.strip()+':'+conf['destination'])
				else:
					rsync.destination(rhost.strip()+'::'+conf['destination'])
				
				if conf['mode'] == 'ssh' and ('remote_before' in conf) :
					before = os.path.expanduser(self.project['cfgdir'] +'/libexec/'+self.domain+'.before')
					if os.path.exists(before) :
						cmd = 'ssh -T '+rhost.strip()+' < '+ before
						screen = os.system(cmd)
						self.logging.debug('BEFORE ' + cmd);
						self.logging.debug(screen)
					else:
						self.logging.error("The file isn't exist: "+before)
				
				overwrite = self.project['cfgdir']+'/'+self.stage+'/resources/'+self.domain
				if os.path.exists(os.path.expanduser(overwrite)):
					cmd = 'cp -a ' + overwrite+'/* '+source
					os.system(cmd)
					self.logging.debug('Overwrite config ' + cmd);
				
				rsync.execute()
				
				if(self.debug):
					self.logging.debug(rsync.debug());
				
				if conf['mode'] == 'ssh' and ('remote_after' in conf):
					after = os.path.expanduser(self.project['cfgdir'] + '/'+self.stage+'/libexec/'+self.domain+'.after')
					if os.path.exists(after):
						cmd = 'ssh -T '+rhost.strip()+' < '+ after
						screen = os.system(cmd)
						self.logging.debug('AFTER ' + cmd)
						self.logging.debug(screen)
					else:
						self.logging.error("The file isn't exist: "+after)

		except NameError as err:
			print(err)
		except KeyError as err:
			print("Error: %s %s" %(err, conf))
		except (FileNotFoundError, IOError) as err:
			print(err)
		except AttributeError as err:
			print(err)
		#	self.logging.error(err)	
	def branch(self, options):
		try:
			conf = self.conf()
			source = os.path.expanduser(conf['source'])
			#if os.path.isdir(source):

			git = Git(source, self.logging)
			if options.checkout:
				git.pull()
				git.branch(options.checkout)
				self.logging.info('checkout branch '+ self.stage+' '+self.domain+' -> '+options.checkout)
			elif options.delete:
				git.branch(options.delete, 'delete')
				self.logging.info('delete branch '+ self.stage+' '+self.domain+' -> '+options.delete)
			elif options.new:
				git.branch(options.new, 'new')
				self.logging.info('create branch '+ self.stage+' '+self.domain+' -> '+options.new)
			elif options.release:
				git.tag(options.release)
			else:
				git.branch()
				self.logging.info('branch '+ self.stage+' '+self.domain)
			#self.logging.debug(git.debug());
			git.execute()

		except configparser.NoSectionError as err:
			self.logging.error(err)
			print(err)
		except FileNotFoundError as err:
			self.logging.error(err)
			print(err)

	def merge(self, t=None, f=None):
		try:
			conf = self.conf()				
			git = Git(conf['source'], self.logging)

			if os.path.exists(conf['source']):
				git.clean('-df').reset().pull().execute()

			if t :
				git.checkout(t).execute()
				git.pull().execute()
			else:
				git.checkout(self.stage).execute()
				git.pull().execute()
				
			if f :
				git.checkout(f).execute()
				git.pull().execute()

				git.merge(f).execute()
				git.push().execute()
				self.logging.info('merge branch to '+ t +' from '+f)
		except Exception as err:
			print("Error: %s %s" %(err, ''))
			sys.exit(1)
	def list(self,stage):
		try:
			config = configparser.SafeConfigParser()
			#if domain == None:
			for file in os.listdir(os.path.expanduser(self.project['cfgdir']+'/'+stage)):
				if file.endswith(".ini"):
					config.read(os.path.expanduser(self.project['cfgdir']+'/'+stage+'/'+file))
					print(file)
					#for project in config.sections():
					#	print(project, end = ', ');
					print(config.sections())
					print()
		except FileNotFoundError as err:
			self.logging.error(err)
			print(err)

class Command():
	debug = False
	stage = ('development','testing','production','stable','unstable','alpha','beta')
	def __init__(self):
		usage = "usage: %prog [options] {branch|stage} project"
		self.parser = OptionParser(usage, version="%prog 1.0.2", description='Software Configuration Management')

		group = OptionGroup(self.parser, "stage", "{"+''.join(self.stage)+"} <host>.<domain>")
		group.add_option("-r", "--revert", dest="revert", default=False,help="revert to revision")
		group.add_option('','--clean', action="store_true", help='')
		group.add_option("-s", "--silent", action="store_true", help="Silent mode. Don't output anything")
		group.add_option("-t", "--trial", action="store_true", default=False, help="perform a trial run with no changes made")
		group.add_option('','--backup', dest="backup", help='backup remote to local', default=None)
		group.add_option('', '--debug', action="store_true", default=False, help="debug mode")
		self.parser.add_option_group(group)

		#group = OptionGroup(self.parser, 'backup', 'backup <host>.<domain>')
		#self.parser.add_option_group(group)

		group = OptionGroup(self.parser, "branch", "branch management")
		group.add_option("-c", "--checkout", dest="checkout", metavar="master|trunk", default='', help="checkout branch")
		group.add_option('-n', "--new", dest="new", metavar="branch", default=None, help="Create new branch")
		group.add_option("-d", "--delete", dest="delete", metavar="branch", help="delete branch")
		group.add_option('','--release', dest="release", help='release version exampe:'+ time.strftime('%Y-%m-%d',time.localtime(time.time())) , default=None)
		self.parser.add_option_group(group)

		group = OptionGroup(self.parser, "merge", "merge {"+''.join(self.stage)+"}")
		group.add_option('','--to', dest='to', metavar='master', help='such as master')
		group.add_option('','--from', dest='froms', metavar='your', help='from branch')
		self.parser.add_option_group(group)

		#group = OptionGroup(self.parser, "build", "build {development | testing | production}")
		#group.add_option('','--build', action="store_true", help='build', default=False)
		#group.add_option('','--package', action="store_true", help='package', default=False)
		#group.add_option('','--install', help='install', default='make')		
		#self.parser.add_option_group(group)

		group = OptionGroup(self.parser, "unittest", "unittest {"+''.join(self.stage)+"}")
		#group.add_option('','--testsuite', help='logs file.', default='backup.log')
		#group.add_option('','--testcase', help='logs file.', default='backup.log')
		self.parser.add_option_group(group)	
	def usage(self):
		self.parser.print_help()
		print("\n  Example: \n\tdeployment testing www.example.com\n\tdeployment production www.example.com --clean\n\tdeployment testing bbs.example.com --backup=/tmp/backup")
		print("\n  Homepage: http://netkiller.github.com\tAuthor: Neo <netkiller@msn.com>")
	#def debug(debug):
	#	self.debug = debug
	def main(self):
		try:
			(options, args) = self.parser.parse_args()
			if options.debug :
				self.debug = True

			if self.debug :
				print("===================================")
				print(options, args)
				#os.getuid()
				#os.getlogin()
				#print(self.config)
				print("===================================")

			if not args:
				self.usage()
			elif args[0] == 'branch':
				if(len(args) == 3):
					stage = args[1]
					domain =args[2]
					deployment = Deployment(stage, domain)
					deployment.debug = self.debug
					deployment.branch(options)
				else:
					self.usage()
			elif args[0] == 'merge':
				if args.__len__() == 2 and args[0] in self.stage:
					stage = args[0]
					domain =args[1]
					deployment = Deployment(self.stage, self.domain)
					deployment.debug = self.debug
					deployment.merge(options.to, options.froms)
				else:
					self.usage()
			elif args[0] in self.stage:
				if args.__len__() == 2:
					self.stage = args[0]
					self.domain =args[1]
					deployment = Deployment(self.stage, self.domain)
					deployment.debug = self.debug
					deployment.deploy(options)
				else:
					self.stage = args[0]
					deployment = Deployment(self.stage, None)
					deployment.debug = self.debug
					deployment.list(self.stage)
			else:
				self.usage()
		except Exception as err:
			print("Error: %s %s" %(err, self.__class__.__name__))
			sys.exit(1)
			
if __name__ == '__main__':
	try:
		command = Command()
		command.debug = True
		command.main()
	except KeyboardInterrupt:
		print ("Crtl+C Pressed. Shutting down.")
