#!/usr/share/ucs-test/runner python3
## desc: |
##  Test filter mechanism for Listener cache
## bugs: [38823]
## packages:
##  - univention-directory-listener (>= 9.0.2-6)
## exposure: dangerous

from os import chown, mkdir
from os.path import join
from pwd import getpwnam
from shutil import rmtree
from socket import create_connection, error as socket_error
from subprocess import Popen
from sys import exit
from tempfile import mkdtemp
from time import sleep

from univention.config_registry.frontend import handler_set
from univention.testing.ucr import UCSTestConfigRegistry
from univention.testing.udm import UCSTestUDM
from univention.testing.utils import verify_ldap_object

MODULE = 'container/ou'
UNIQUE = 'ucs-test38823'
TMPDIR = '/tmp'
USERID = 'listener'


class Environment(object):

	def __init__(self):
		self.tmpdir = None
		self.mdir = None
		self.cdir = None

		ent = getpwnam(USERID)
		self.uid = ent.pw_uid

	def __enter__(self):
		self.tmpdir = mkdtemp(prefix='ucs-test', dir=TMPDIR)
		print('I: tmpdir=%r' % (self.tmpdir,))
		chown(self.tmpdir, self.uid, -1)

		self.mdir = self.mkdir('module')
		self.cdir = self.mkdir('cache')

		self.copy_handler()
		return self

	def mkdir(self, name):
		path = join(self.tmpdir, name)
		mkdir(path)
		chown(path, self.uid, -1)
		return path

	def copy_handler(self):
		with open(argv[0], 'r') as source:
			with open(join(self.mdir, 'filter.py'), 'w') as target:
				for line in source:
					if line.startswith('TMPDIR = '):
						line = 'TMPDIR = %r\n' % (self.tmpdir,)
					target.write(line)

	def __exit__(self, exc_type, exc_value, traceback):
		rmtree(self.tmpdir, ignore_errors=True)
		self.tmpdir = None


class Listener(object):

	def __init__(self, ucr, env):
		self.ucr = ucr
		self.env = env
		self.proc = None
		self.cmd = [
			'/usr/sbin/univention-directory-listener',
			'-b', self.ucr['ldap/base'],
			'-m', self.env.mdir,
			'-c', self.env.cdir,
			'-x',
			'-ZZ',
			'-D', self.ucr['ldap/hostdn'],
			'-y', '/etc/machine.secret',
		]

	def __enter__(self):
		self.init_listener()
		self.run_listener()
		return self

	def init_listener(self):
		cmd = self.cmd + [
			'-d', '2',
			'-i',
		]
		proc = Popen(cmd, close_fds=True)
		print('I: %d = %r' % (proc.pid, cmd,))
		ret = proc.wait()
		print('I: ret=%r' % (ret,))
		if ret:
			exit(ret)

	def run_listener(self):
		cmd = self.cmd + [
			'-d', '4',
			'-F',
		]
		self.proc = Popen(cmd, close_fds=True)
		print('I: %d = %r' % (self.proc.pid, cmd,))
		return self

	def __exit__(self, exc_type, exc_value, traceback):
		if not exc_type:
			self.wait_transactions()
		self.kill_listener()

		ret = self.proc.wait()
		print('I: ret=%d' % (ret,))
		self.proc = None

	def wait_transactions(self):
		prev_master = 0
		i = 0
		while i < 10:
			master = self.get_master_transaction()
			local = self.get_local_transaction()
			print("I: %d <= %d >= %d %d..." % (local, master, prev_master, i))
			if local == master:
				return
			i = i + 1 if master == prev_master else 0
			prev_master = master
			sleep(1)

	def get_master_transaction(self):
		hostname = self.ucr['ldap/master']
		try:
			sock = create_connection((hostname, 6669), 60.0)

			sock.send(b'Version: 3\nCapabilities: \n\n')
			sock.recv(100)

			sock.send(b'MSGID: 1\nGET_ID\n\n')
			result = sock.recv(100).decode('ASCII')

			sock.close()
		except socket_error as ex:
			print('E: error talking to UDN on %s: %s' % (hostname, ex))
			return

		lines = result.splitlines()
		return int(lines[1])

	def get_local_transaction(self):
		filename = join(self.env.cdir, 'notifier_id')
		with open(filename, 'r') as nid_file:
			nid = nid_file.read()
		return int(nid)

	def kill_listener(self):
		if not self.proc:
			return

		self.proc.terminate()
		for i in range(6):
			ret = self.proc.poll()
			print('I: ret=%s %d...' % (ret, i,))
			if ret is not None:
				break
			sleep((1 << i) / 10.0)
		else:
			self.proc.kill()


def main():
	error = False
	with Environment() as env:
		with UCSTestConfigRegistry() as ucr:
			handler_set(
				['listener/cache/filter=(ou=%s)' % (UNIQUE,)],
				opts={'schedule': True},
				quiet=False)

			with Listener(ucr, env) as listener:
				with UCSTestUDM() as udm:
					ou = udm.create_object(MODULE, name=UNIQUE)
					udm.modify_object(MODULE, dn=ou, description=UNIQUE)
					listener.wait_transactions()
					verify_ldap_object(ou)

			error |= unexpected_transactions(env, ucr)
			error |= is_not_in_cache(env, ucr)

	exit(1 if error else 0)


def unexpected_transactions(env, ucr):
	found_add = False
	found_modify = False
	found_other = False

	dn = 'ou=%s,%s' % (UNIQUE, ucr['ldap/base'])

	with open(join(env.tmpdir, UNIQUE), 'r') as log:
		for line in log:
			line = line.strip()
			if line == repr((dn, True, False, 'a')):
				print('I: Found add: %s' % (line,))
				found_add = True
			elif line == repr((dn, True, False, 'm')):
				print('I: Found modify: %s' % (line,))
				found_modify = True
			elif eval(line)[0].endswith(dn):
				found_other = True
				print('E: Found other: %s' % (line,))

	return found_other or not found_add or not found_modify


def is_not_in_cache(env, ucr):
	error = False

	dn = 'dn: ou=%s,%s' % (UNIQUE, ucr['ldap/base'])

	dump = join(env.tmpdir, 'dump')
	cmd = [
		'/usr/sbin/univention-directory-listener-dump',
		'-c', env.cdir,
		'-O', dump,
	]
	proc = Popen(cmd, close_fds=True)
	print('I: %d = %r' % (proc.pid, cmd,))
	proc.wait()

	with open(dump, 'r') as cache:
		for line in cache:
			line = line.strip()
			if line == dn:
				error = True
				print('E: Found DN: %s' % (line,))
			elif UNIQUE in line:
				print('W: Found line: %s' % (line,))

	return error


def handler(dn, new, old, cmd=''):
	with open(join(TMPDIR, UNIQUE), 'a') as log:
		print(repr((dn, bool(new), bool(old), cmd)), file=log)


if __name__ == '__main__':
	from sys import argv
	main()
else:
	name = "filter"
	description = "Test filter mechanism for Listener cache"
	filter = "(objectClass=organizationalUnit)"
	attributes = []
	modrdn = "1"

# vim:set ft=python:
