#!/usr/bin/python3
#
# Univention AD Connector
# the main start script
#
# SPDX-FileCopyrightText: 2004-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only
import contextlib
import fcntl
import os
import signal
import sys
import time
import traceback
from argparse import ArgumentParser
import ldap
import univention.connector
import univention.connector.ad
from univention.config_registry import ConfigRegistry
[docs]
@contextlib.contextmanager
def bind_stdout(options, statuslogfile):
if options.daemonize:
with open(statuslogfile, 'w+') as sys.stdout:
yield
else:
yield
[docs]
def daemon(lock_file, options):
try:
pid = os.fork()
except OSError as e:
print(f'Daemon Mode Error: {e.strerror}')
if pid == 0:
os.setsid()
signal.signal(signal.SIGHUP, signal.SIG_IGN)
try:
pid = os.fork()
except OSError as e:
print(f'Daemon Mode Error: {e.strerror}')
if pid == 0:
os.chdir("/")
os.umask(0o027)
else:
pf = open(f'/var/run/univention-ad-{options.configbasename}', 'w+')
pf.write(str(pid))
pf.close()
os._exit(0)
else:
os._exit(0)
try:
maxfd = os.sysconf("SC_OPEN_MAX")
except (AttributeError, ValueError):
maxfd = 256 # default maximum
for fd in range(maxfd):
if fd == lock_file.fileno():
continue
try:
os.close(fd)
except OSError: # ERROR (ignore)
pass
os.open("/dev/null", os.O_RDONLY)
os.open("/dev/null", os.O_RDWR)
os.open("/dev/null", os.O_RDWR)
[docs]
def connect(options):
print(time.ctime())
ucr = ConfigRegistry()
ucr.load()
poll_sleep = int(ucr[f'{options.configbasename}/ad/poll/sleep'])
ad_init = None
while not ad_init:
try:
ad = univention.connector.ad.ad.main(ucr, options.configbasename, logfilename=options.log_file, debug_level=options.debug)
ad.init_ldap_connections()
ad.init_group_cache()
ad_init = True
except ldap.SERVER_DOWN:
print("Warning: Can't initialize LDAP-Connections, wait...")
sys.stdout.flush()
time.sleep(poll_sleep)
# log the active mapping
with open(f'/var/log/univention/{options.configbasename}-ad-mapping.log', 'w+') as fd:
print(repr(univention.connector.Mapping(ad.property)), file=fd)
with ad as ad:
_connect(ad, poll_sleep, ucr.get(f'{options.configbasename}/ad/retryrejected', 10))
def _connect(ad, poll_sleep, baseconfig_retry_rejected):
# Initialisierung auf UCS und AD Seite durchfuehren
ad_init = None
ucs_init = None
while not ucs_init:
try:
ad.initialize_ucs()
ucs_init = True
except ldap.SERVER_DOWN:
print("Can't contact LDAP server during ucs-poll, sync not possible.")
sys.stdout.flush()
time.sleep(poll_sleep)
ad.open_ad()
ad.open_ucs()
while not ad_init:
try:
ad.initialize()
ad_init = True
except ldap.SERVER_DOWN:
print("Can't contact LDAP server during ucs-poll, sync not possible.")
sys.stdout.flush()
time.sleep(poll_sleep)
ad.open_ad()
ad.open_ucs()
retry_rejected = 0
connected = True
while connected:
print(time.ctime())
# Aenderungen pollen
sys.stdout.flush()
while True:
# Read changes from OpenLDAP
try:
change_counter = ad.poll_ucs()
if change_counter > 0:
# UCS changes, read again from UCS
retry_rejected = 0
time.sleep(1)
continue
else:
break
except ldap.SERVER_DOWN:
print("Can't contact LDAP server during ucs-poll, sync not possible.")
connected = False
sys.stdout.flush()
break
while True:
try:
change_counter = ad.poll()
if change_counter > 0:
# AD changes, read again from AD
retry_rejected = 0
time.sleep(1)
continue
else:
break
except ldap.SERVER_DOWN:
print("Can't contact LDAP server during ad-poll, sync not possible.")
connected = False
sys.stdout.flush()
break
try:
if str(retry_rejected) == baseconfig_retry_rejected: # FIXME: if the UCR variable is not set this compares string with integer (default value)
ad.resync_rejected_ucs()
ad.resync_rejected()
retry_rejected = 0
else:
retry_rejected += 1
except ldap.SERVER_DOWN:
print("Can't contact LDAP server during resync rejected, sync not possible.")
connected = False
sys.stdout.flush()
change_counter = 0
retry_rejected += 1
print(f'- sleep {poll_sleep} seconds ({retry_rejected}/{baseconfig_retry_rejected} until resync) -')
sys.stdout.flush()
time.sleep(poll_sleep)
[docs]
@contextlib.contextmanager
def lock(filename):
try:
lock_file = open(filename, "a+")
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
print('Error: Another AD connector process is already running.', file=sys.stderr)
sys.exit(1)
with lock_file as lock_file:
yield lock_file
[docs]
def main():
parser = ArgumentParser()
parser.add_argument("--configbasename", help="", metavar="CONFIGBASENAME", default="connector")
parser.add_argument('-n', '--no-daemon', dest='daemonize', default=True, action='store_false', help='Start process in foreground')
parser.add_argument('-d', '--debug', help='debug level', type=int)
parser.add_argument('-L', '--log-file', metavar='LOGFILE', help='Specifies an alternative logfile')
options = parser.parse_args()
with lock(f'/var/lock/univention-ad-{options.configbasename}') as lock_file:
if options.daemonize:
daemon(lock_file, options)
with bind_stdout(options, f"/var/log/univention/{options.configbasename}-ad-status.log"):
while True:
try:
connect(options)
except Exception:
print(time.ctime())
print(" --- connect failed, failure was: ---")
print(traceback.format_exc())
print(" --- retry in 30 seconds ---")
sys.stdout.flush()
time.sleep(30)
if __name__ == "__main__":
main()