#!/usr/bin/python3 -OO
# -*- coding: utf-8 -*-
#
# Univention UCS@school
#
# SPDX-FileCopyrightText: 2007-2026 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only

from __future__ import print_function

import ipaddress
import optparse
import os
import re
import sys
import time
import traceback

import ldap
import ldap.filter
from ldap.dn import dn2str, escape_dn_chars, str2dn
from ldap.filter import filter_format

import univention.admin.modules
import univention.admin.objects
import univention.admin.uexceptions
import univention.admin.uldap
import univention.config_registry
import univention.debug as ud
from ucsschool.importer.utils.utils import verify_school_ou
from ucsschool.lib.models.utils import add_stream_logger_to_schoollib, create_passwd, stopped_notifier
from ucsschool.lib.roles import (
    create_ucsschool_role_string,
    role_dc_slave_admin,
    role_dc_slave_edu,
    role_school_admin_group,
    role_school_class,
    role_school_class_share,
)

# -> doesn't do:
# check usernames for correct values (no spaces)
# change the user's position in the tree (change the school i.e.)
# select Schools that are separated by kommata
# modify users school or type (pupil/teacher)

# -> needs definition:
# import access rights

# string of problems during process
problem_hints = ""
# separator char in infile (default: \t for Tabulator)
sepchar = "\t"

# dict of existing mail domains
mailDomains = {}

ud.init("/var/log/univention/ucs-school-import.log", ud.FLUSH, ud.NO_FUNCTION)
ud.set_level(ud.MAIN, ud.ALL)

ucr = univention.config_registry.ConfigRegistry()
ucr.load()

add_stream_logger_to_schoollib(stream=sys.stdout, name="legacy-import")

baseDN = ucr["ldap/base"]
domainname = ucr["domainname"]

emptyLineRe = re.compile(r"^\W*$")

district_enabled = False
if ucr.is_true("ucsschool/ldap/district/enable"):
    district_enabled = True

# create edukativ DC by default
school_dcs = ucr.get("ucsschool/ldap/default/dcs", "edukativ")

verified_ous = []
verified_groups = []
verified_group_shares = []

cn_pupils = ucr.get("ucsschool/ldap/default/container/pupils", "schueler")
cn_teachers = ucr.get("ucsschool/ldap/default/container/teachers", "lehrer")
cn_teachers_staff = ucr.get(
    "ucsschool/ldap/default/container/teachers-and-staff", "lehrer und mitarbeiter"
)
cn_admins = ucr.get("ucsschool/ldap/default/container/admins", "admins")
cn_staff = ucr.get("ucsschool/ldap/default/container/staff", "mitarbeiter")
cn_legal_guardians = ucr.get("ucsschool/ldap/default/container/legal_guardians")

grp_prefix_pupils = ucr.get("ucsschool/ldap/default/groupprefix/pupils", "schueler-")
grp_prefix_teachers = ucr.get("ucsschool/ldap/default/groupprefix/teachers", "lehrer-")
grp_prefix_admins = ucr.get("ucsschool/ldap/default/groupprefix/admins", "admins-")
grp_prefix_staff = ucr.get("ucsschool/ldap/default/groupprefix/staff", "mitarbeiter-")
grp_prefix_legal_guardians = ucr.get("ucsschool/ldap/default/groupprefix/legal_guardians")


TYPE_DC_ADMINISTRATIVE = "administrative"
TYPE_DC_EDUCATIONAL = "educational"


# IP address prefix len conecerning the netmask
default_prefixlen = 24

if not (
    cn_pupils and cn_teachers and cn_teachers_staff and cn_admins and cn_staff and cn_legal_guardians
):
    cn_ucr = {
        "ucsschool/ldap/default/container/pupils": cn_pupils,
        "ucsschool/ldap/default/container/teachers": cn_teachers,
        "ucsschool/ldap/default/container/teachers-and-staff": cn_teachers_staff,
        "ucsschool/ldap/default/container/staff": cn_staff,
        "ucsschool/ldap/default/container/admins": cn_admins,
        "ucsschool/ldap/default/container/legal_guardians": cn_legal_guardians,
    }
    print(
        "ERROR: Unable to proceed: the following UCR variables are not set correctly:\n{}".format(
            "\n".join(key for (key, value) in cn_ucr.items() if not value)
        )
    )
    sys.exit(1)


def is_valid_ou_name(name):
    """check if given OU name is valid"""
    return bool(re.match(r"^[a-zA-Z0-9](([a-zA-Z0-9_]*)([a-zA-Z0-9]$))?$", name))


def is_valid_hostname(name):
    """check if given hostname is valid"""
    return bool(re.match(r"^[a-zA-Z0-9](([a-zA-Z0-9-_]*)([a-zA-Z0-9]$))?$", name))


# exception classes
class CreateObjectError(Exception):
    pass


def extract_district(schoolNr):
    return schoolNr[:2]


def getDN(schoolNr, base="school", basedn=baseDN):
    """
    @param base Values are either school, district or base
    @return According to the base a specific part of dn is returned.
        Let's suppose the following school dn: ou=SCHOOL,ou=DISTRICT,dc=BASE,dc=DN
        The following is returned:
            'base'      -> dc=BASE,dc=DN
            'district'  -> ou=DISTRICT,dc=BASE,dc=DN
            'school'    -> ou=SCHOOL,ou=DISTRICT,dc=BASE,dc=DN
    """
    dn = "%(school)s%(district)s%(basedn)s"
    values = {"school": "ou=%s," % escape_dn_chars(schoolNr), "district": "", "basedn": basedn}
    if district_enabled:
        district = extract_district(schoolNr)
        if not district:
            print(
                "ERROR: Unable to continue execution without district number. School number: %s"
                % schoolNr
            )
            sys.exit(1)
        values["district"] = "ou=%s," % district
    if base == "district":
        values["school"] = ""
    elif base == "base":
        values["district"] = ""
        values["school"] = ""
    return dn % values


def create_object(o, ignore_exists=False):
    exists = False
    msg = ""
    dn = None
    try:
        dn = o.create()
        print("creating object", dn)
    except univention.admin.uexceptions.objectExists:
        if not ignore_exists:
            msg = "ERROR: Object exists-1 (objectExists)"
        else:
            exists = True
    except univention.admin.uexceptions.uidAlreadyUsed:
        if not ignore_exists:
            msg = "ERROR: Object exists-2 (uidAlreadyUsed)"
        else:
            exists = True
    except univention.admin.uexceptions.dhcpServerAlreadyUsed:
        if not ignore_exists:
            msg = "ERROR: Object exists-3 (dhcpServerAlreadyUsed)"
        else:
            exists = True
    except univention.admin.uexceptions.noLock:
        if not ignore_exists:
            msg = "ERROR: Object exists-4 (noLock)"
        else:
            exists = True

    if msg:
        print(msg)
        raise CreateObjectError(exists, dn)

    return (exists, dn)


def verify_containers(position_dn, module, co, lo, superordinate, base, path=""):
    """
    verifies all containers starting at the ldap base

    @param module Param is ignored! cn_module is the only supported module!!
    """
    split_dn = str2dn(position_dn)
    # with this variable it's ensured that within a row of containers there is
    # no other object/container which can not be created with method
    started_cn_row = False
    for i, part in enumerate(reversed(split_dn)):
        if part[0][0] == "cn":
            verify_container(
                dn2str(split_dn[-i - 1 :]), cn_module, None, lo, superordinate, base, path=path
            )
            started_cn_row = True
        elif started_cn_row:
            # found an object/container that can not be created
            break


def verify_container(position_dn, module, co, lo, superordinate, base, path="userPath"):
    """look if goal-container exists, else create it"""
    container_exists = 0
    attr = str2dn(position_dn)[0][0][0]
    name = str2dn(position_dn)[0][0][1]
    position = lo.parentDn(position_dn)
    try:
        objects = univention.admin.modules.lookup(
            module,
            None,
            lo,
            scope="sub",
            superordinate=superordinate,
            base=position,
            filter=univention.admin.filter.expression(attr, name, escape=True),
        )
    except (univention.admin.uexceptions.noObject, ldap.NO_SUCH_OBJECT):
        objects = []
    for object in objects:
        if object.dn.lower() == position_dn.lower():
            container_exists = 1
            break

    created = False
    dn = None
    if not container_exists:
        print("need to create container %s" % position_dn)
        position = univention.admin.uldap.position(base)
        position.setDn(lo.parentDn(position_dn))
        object = module.object(None, lo, position, superordinate=superordinate)
        object.open()
        object["name"] = name

        if attr == "cn" and path and name != "users":  # is path container
            object[path] = "1"

        try:
            exists, dn = create_object(object)
            created = True
        except CreateObjectError as e:
            exists, dn = e.args

    return (created, dn)


def verify_group_share(schoolNr, classNr, co, lo, superordinate, base):
    if (schoolNr, classNr.lower()) in verified_group_shares:
        return True

    position_dn = "cn=%s,cn=klassen,cn=shares,%s" % (
        escape_dn_chars(classNr),
        getDN(schoolNr, basedn=base),
    )
    module = univention.admin.modules.get("shares/share")
    position_basedn = univention.admin.uldap.position(baseDN)
    univention.admin.modules.init(lo, position_basedn, module)
    more_instances = 1
    container_position = base
    while more_instances:
        container_position = dn2str(str2dn(position_dn)[-len(str2dn(container_position)) - 1 :])
        container_attr = str2dn(container_position)[0][0][0]
        if container_position == position_dn:
            more_instances = 0
        elif container_attr == "cn":
            verify_container(
                container_position, cn_module, None, lo, superordinate, base, path="groupPath"
            )
        elif container_attr == "ou":
            verify_container(container_position, ou_module, None, lo, superordinate, base)
        else:
            print("WARNING: unknown container type", container_position)

    # look if share exists, else create it
    share_exists = 0
    attr = str2dn(position_dn)[0][0][0]
    name = str2dn(position_dn)[0][0][1]
    position = lo.parentDn(position_dn)
    objects = univention.admin.modules.lookup(
        module,
        None,
        lo,
        scope="sub",
        superordinate=superordinate,
        base=position,
        filter=univention.admin.filter.expression(attr, name, escape=True),
    )
    if objects:
        for object in objects:
            if object.dn.lower() == position_dn.lower():
                share_exists = 1
                break

    if not share_exists:
        print("need to create groupshare %s" % position_dn)

        # get gid form corresponding group
        group_dn = "cn=%s,cn=klassen,cn=%s,cn=groups,%s" % (
            escape_dn_chars(classNr),
            escape_dn_chars(cn_pupils),
            getDN(schoolNr, basedn=base),
        )
        gids = lo.get(group_dn, ["gidNumber"])
        gid = 0
        if len(gids) > 1:  # TODO FIXME This doesn't look correct to me - gids is a dict and not a list!
            print("WARNING: more than one corresponding gid found")
            gid = gids["gidNumber"][0].decode("ASCII")
        elif (
            len(gids) < 1
        ):  # TODO FIXME This doesn't look correct to me - gids is a dict and not a list!
            print("WARNING: no corresponding gid found")
        else:
            gid = gids["gidNumber"][0].decode("ASCII")

        # set default server
        serverfqdn = "dc%s.%s" % (schoolNr.lower(), domainname)

        # get alternative server (defined at ou object if a Replica Directory Node is responsible for more than one ou)  # noqa: E501
        ou_dn = getDN(schoolNr, basedn=base)
        ou_attr_LDAPAccessWrite = lo.get(ou_dn, ["univentionLDAPAccessWrite"])
        alternativeServer_dn = None
        if len(ou_attr_LDAPAccessWrite) > 0:
            alternativeServer_dn = ou_attr_LDAPAccessWrite["univentionLDAPAccessWrite"][0].decode(
                "UTF-8"
            )
            if (
                len(ou_attr_LDAPAccessWrite) > 1
            ):  # TODO FIXME This doesn't look correct to me - ou_attr_LDAPAccessWrite is a dict and not a list!  # noqa: E501
                print(
                    "WARNING: more than one corresponding univentionLDAPAccessWrite found at ou=%s"
                    % schoolNr
                )

        # build fqdn of alternative server and set serverfqdn
        if alternativeServer_dn:
            alternativeServer_attr = lo.get(alternativeServer_dn, ["uid"])
            if len(alternativeServer_attr) > 0:
                alternativeServer_uid = alternativeServer_attr["uid"][0].decode("UTF-8")
                alternativeServer_uid = alternativeServer_uid.replace("$", "")
                if len(alternativeServer_uid) > 0:
                    serverfqdn = "%s.%s" % (alternativeServer_uid, domainname)
        # fetch serverfqdn from OU
        result = lo.get(getDN(schoolNr, basedn=base), ["ucsschoolClassShareFileServer"])
        if result:
            serverDomainName = lo.get(
                result["ucsschoolClassShareFileServer"][0].decode("UTF-8"), ["associatedDomain"]
            )
            if serverDomainName:
                serverDomainName = serverDomainName["associatedDomain"][0].decode("UTF-8")
            else:
                serverDomainName = domainname
            result = lo.get(result["ucsschoolClassShareFileServer"][0].decode("UTF-8"), ["cn"])
            if result:
                serverfqdn = "%s.%s" % (result["cn"][0].decode("UTF-8"), serverDomainName)

        position = univention.admin.uldap.position(base)
        position.setDn(lo.parentDn(position_dn))
        object = module.object(None, lo, position, superordinate=superordinate)
        object.open()
        object["name"] = "%s" % classNr
        object["host"] = serverfqdn
        if ucr.is_true("ucsschool/import/roleshare", True):
            object["path"] = "/home/" + os.path.join(schoolNr, "groups/klassen/%s" % (classNr,))
        else:
            object["path"] = "/home/groups/klassen/%s" % (classNr,)
        object["writeable"] = "1"
        object["sambaWriteable"] = "1"
        object["sambaBrowseable"] = "1"
        object["sambaForceGroup"] = "+%s" % classNr
        object["sambaCreateMode"] = "0770"
        object["sambaDirectoryMode"] = "0770"
        object["owner"] = "0"
        object["group"] = gid
        object["directorymode"] = "0770"
        object["ucsschoolRole"] = [create_ucsschool_role_string(role_school_class_share, schoolNr)]
        if ucr.is_false("ucsschool/default/share/nfs", True):
            try:
                object.options.remove("nfs")  # deactivate NFS
            except ValueError:
                pass
        create_object(object)

    if classNr not in verified_group_shares:
        verified_group_shares.append((schoolNr, classNr.lower()))


def verify_group(
    position_dn, co, lo, superordinate, base, schoolNr, is_school_class=False, is_admin_group=False
):
    # look if group exists, else create it

    if position_dn in verified_groups:
        return True

    group_exists = 0
    attr = str2dn(position_dn)[0][0][0]
    name = str2dn(position_dn)[0][0][1]
    position = lo.parentDn(position_dn)

    more_instances = 1
    container_position = base
    while more_instances:
        container_position = dn2str(str2dn(position_dn)[-len(str2dn(container_position)) - 1 :])
        container_attr = str2dn(container_position)[0][0][0]
        if container_position == position_dn:
            more_instances = 0
        elif container_attr == "cn":
            verify_container(
                container_position, cn_module, None, lo, superordinate, base, path="groupPath"
            )
        elif container_attr == "ou":
            verify_container(container_position, ou_module, None, lo, superordinate, base)
        else:
            print("WARNING: unknown container type", container_position)

    objects = univention.admin.modules.lookup(
        group_module,
        None,
        lo,
        scope="sub",
        superordinate=superordinate,
        base=position,
        filter=univention.admin.filter.expression(attr, name, escape=True),
    )
    if objects:
        for object in objects:
            if object.dn.lower() == position_dn.lower():
                group_exists = 1
                break

    if not group_exists:
        if is_admin_group:
            create_what = "admin group"
        elif is_school_class:
            create_what = "school class"
        else:
            create_what = "group"
        print("need to create {} {}".format(create_what, position_dn))
        position = univention.admin.uldap.position(base)
        position.setDn(lo.parentDn(position_dn))
        object = group_module.object(None, lo, position, superordinate=superordinate)
        object.open()
        object["name"] = name
        if is_school_class:
            object["ucsschoolRole"] = [create_ucsschool_role_string(role_school_class, schoolNr)]
        elif is_admin_group:
            object["ucsschoolRole"] = [create_ucsschool_role_string(role_school_admin_group, schoolNr)]
        create_object(object)

    if position_dn not in verified_groups:
        verified_groups.append(position_dn)


def delete_dn(dn, module, co, lo):
    try:
        lo.search(base=dn, scope="base")
    except univention.admin.uexceptions.noObject:
        return

    position.setDn(lo.parentDn(dn))
    object = univention.admin.objects.get(module, None, lo, position=position, dn=dn)
    object.open()
    object.remove()
    print("deleted:", dn)
    return True


def create_network(
    schoolNr, network, iprange=None, defaultrouter=None, nameserver=None, netbiosserver=None
):
    """
    @param network The network address MUST contain the netmask!! it could also be a ipaddress.IPv4Network
        object
    @param defaultrouter IP-Address, if no netmask is profided it's derived from network
    @param nameserver IP-Address, if no netmask is profided it's derived from network
    @param netbiosserver IP-Address, if no netmask is profided it's derived from network
    """  # noqa: E501
    assert isinstance(network, ipaddress.IPv4Network)
    success = True
    dn = None
    verify_school_ou(schoolNr, lo)

    if check_network(schoolNr, network):
        print("Network %s exists in school %s!" % (network.network_address, schoolNr))
        return (False, dn)

    print("generate network %s" % (network.network_address,))
    if iprange:
        print("iprange: %s-%s" % (iprange[0], iprange[1]))
    if defaultrouter:
        print("defaultrouter: %s" % defaultrouter)
    if nameserver:
        print("nameserver: %s" % nameserver)
    if netbiosserver:
        print("netbiosserver: %s" % netbiosserver)

    # WORKAROUND for Bug #14795
    subnetbytes = 0
    tmp = str(network.netmask).split(".")
    for i in tmp:
        if i == "255":
            subnetbytes += 1
        else:
            break
    subnet = ".".join(str(network.network_address).split(".")[:subnetbytes])
    # END WORKAROUND

    position.setDn("cn=dns,%s" % (baseDN))
    object = dns_reverse_zone_module.object(None, lo, position=position, superordinate=superordinate)
    object.open()
    object["subnet"] = subnet
    # the nameserver/SOA at the dns_reverse_zone object is always the ldap/master server!
    object["nameserver"] = ucr["ldap/master"]
    object["contact"] = "root@%s" % domainname
    try:
        create_object(object, ignore_exists=True)
    except CreateObjectError:
        pass

    position.setDn("cn=%s,cn=dhcp,%s" % (escape_dn_chars(schoolNr.lower()), getDN(schoolNr)))
    dhcp_service_objects = univention.admin.modules.lookup(
        dhcp_service_module,
        None,
        lo,
        scope="sub",
        superordinate=superordinate,
        base=baseDN,
        filter=filter_format("cn=%s", [schoolNr.lower()]),
    )
    if not dhcp_service_objects:
        print("ERROR: DHCP service object for %r not found" % (schoolNr.lower(),))
        success = False
    else:
        object = dhcp_subnet_module.object(
            None, lo, position=position, superordinate=dhcp_service_objects[0]
        )
        object.open()
        object["subnet"] = str(network.network_address)
        object["subnetmask"] = str(network.netmask)
        object["broadcastaddress"] = str(network.broadcast_address)
        try:
            create_object(object, ignore_exists=True)
        except CreateObjectError:
            print("%r already existed - usually no problem" % (object.dn,))

    position.setDn("cn=networks,%s" % (getDN(schoolNr),))
    object = network_module.object(None, lo, position=position, superordinate=superordinate)
    object.open()
    object["name"] = "%s-%s" % (schoolNr.lower(), network.network_address)
    object["netmask"] = str(network.prefixlen)
    object["network"] = str(network.network_address)
    if iprange:
        object["ipRange"] = [[str(iprange[0]), str(iprange[1])]]
    object["dhcpEntryZone"] = "cn=%s,cn=dhcp,%s" % (escape_dn_chars(schoolNr.lower()), getDN(schoolNr))
    object["dnsEntryZoneForward"] = "zoneName=%s,cn=dns,%s" % (escape_dn_chars(domainname), baseDN)
    object["dnsEntryZoneReverse"] = "zoneName=%s.in-addr.arpa,cn=dns,%s" % (
        escape_dn_chars(".".join(reversed(subnet.split(".")))),
        baseDN,
    )

    try:
        exists, dn = create_object(object, ignore_exists=True)
    except CreateObjectError:
        success = False

    # set netbios and router for dhcp subnet
    if defaultrouter:
        print("setting default router")
        set_router_for_subnet(network, defaultrouter, schoolNr)

    if netbiosserver:
        print("setting netbios server")
        set_netbiosserver_for_subnet(network, netbiosserver, schoolNr)

    # set default value for nameserver
    if nameserver:
        print("setting nameserver")
        set_nameserver_for_subnet(network, nameserver, schoolNr)

    return (success, dn)


def check_network(schoolNr, network):
    """@param network ipaddress.IPv4Network object"""
    objects = univention.admin.modules.lookup(
        network_module,
        None,
        lo,
        scope="sub",
        superordinate=superordinate,
        base="cn=networks,%s" % (getDN(schoolNr)),
        filter=filter_format("cn=%s", ["%s-%s" % (schoolNr.lower(), network.network_address)]),
    )
    if objects:
        n = "cn=%s-%s,cn=networks,%s" % (
            escape_dn_chars(schoolNr.lower()),
            escape_dn_chars(network.network_address),
            getDN(schoolNr),
        )
        for object in objects:
            if object.dn == n:
                return 1

    return None


def get_computer_dn(name):
    objects = univention.admin.modules.lookup(
        computer_module,
        None,
        lo,
        scope="sub",
        superordinate=superordinate,
        filter=filter_format("cn=%s", [name]),
    )
    if objects:
        for object in objects:
            if str2dn(object.dn)[0][0][1] == name:
                return object.dn
    return None


def import_networks(router_only=False):
    # usage
    usage = "%prog [options] <importFile>\n"
    if router_only:
        usage += "  modifies default routers according to import file for UCS@school\n\n"
    else:
        usage += "  creates network objects according to import file for UCS@school\n\n"
    usage += "Arguments:\n"
    usage += "  <importFile> network import file"
    parser = optparse.OptionParser(usage=usage)
    options, args = parser.parse_args()

    # argument is infile
    if len(args) > 0:
        infile = args[0]
    else:
        parser.print_help()
        print("must have one Argument for infile")
        sys.exit()

    print("infile is  : " + infile)

    with open(infile) as inf, stopped_notifier():
        for line in inf:
            if emptyLineRe.match(line):
                continue

            success = True
            parsed = line.strip("\r\n").split(sepchar)

            schoolNr = parsed[0]
            verify_school_ou(schoolNr, lo)
            # the network MUST include the netmask
            network = parsed[1]  # netmask new
            # the broadcast address is generated automatically by the
            # combination of network and netmask

            def parseString(parsed, idx, default=None):
                res = default
                if len(parsed) > idx:
                    string = parsed[idx]
                    if string:
                        res = string
                return res

            iprange = parseString(parsed, 2)  # new
            defaultrouter = parseString(parsed, 3)  # new
            nameserver = parseString(parsed, 4)  # new
            netbiosserver = parseString(parsed, 5)  # new

            dn = None
            success = True

            # convert parameters to ipaddress.IPv4Network
            if "/" not in network:
                network = "%s/%s" % (network, default_prefixlen)
                try:
                    network = ipaddress.IPv4Network(network, strict=False)
                except ipaddress.AddressValueError as e:
                    print(line.strip())
                    print("ERROR: %s is not a valid ip address" % e)
                    continue
                print(
                    "WARNING: no netmask specified for network %s using %s"
                    % (
                        network,
                        network.netmask,
                    )
                )
            else:
                try:
                    network = ipaddress.IPv4Network("%s" % (network,), strict=False)
                except ipaddress.AddressValueError as e:
                    print(line.strip())
                    print("ERROR: %s is not a valid ip address" % e)
                    continue
                except ipaddress.NetmaskValueError as e:
                    print(line.strip())
                    print("ERROR: %s is not a valid netmask" % e)
                    continue

            if iprange:
                tmp = iprange.split("-")
                if len(tmp) != 2:
                    print("ERROR: IP host range not valid: %s" % iprange)
                    continue
                iprangeStart = ""
                iprangeEnd = ""
                try:
                    iprangeStart = ipaddress.IPv4Address("%s" % (tmp[0],))
                except ipaddress.AddressValueError as e:
                    print(line.strip())
                    print("ERROR: %s is not a valid ip address" % e)
                    continue
                try:
                    iprangeEnd = ipaddress.IPv4Address("%s" % (tmp[1],))
                except ipaddress.AddressValueError as e:
                    print(line.strip())
                    print("ERROR: %s is not a valid ip address" % e)
                    continue
                iprange = (iprangeStart, iprangeEnd)
                if (
                    iprange[0] < network.network_address + 1
                    or iprange[0] > iprange[1]
                    or iprange[1] >= network.broadcast_address
                ):
                    print(line.strip())
                    print("ERROR: IP host range not valid: %s-%s" % (iprange[0], iprange[1]))
                    continue
            elif network.prefixlen == 24:
                iprange = (network.network_address + 20, network.network_address + 250)

            if defaultrouter:
                try:
                    defaultrouter = ipaddress.IPv4Address("%s" % (defaultrouter,))
                except ipaddress.AddressValueError as e:
                    print(line.strip())
                    print("ERROR: %s is not a valid ip address" % e)
                    continue
            if nameserver:
                try:
                    nameserver = ipaddress.IPv4Address("%s" % (nameserver,))
                except ipaddress.AddressValueError as e:
                    print(line.strip())
                    print("ERROR: %s is not a valid ip address" % e)
                    continue
            if netbiosserver:
                try:
                    netbiosserver = ipaddress.IPv4Address("%s" % (netbiosserver,))
                except ipaddress.AddressValueError as e:
                    print(line.strip())
                    print("ERROR: %s is not a valid ip address" % e)
                    continue

            if router_only:
                if defaultrouter:
                    success, dn = set_router_for_subnet(
                        network, defaultrouter, schoolNr, overwrite_policy=True
                    )
                    print("Router set for subnet.")
                else:
                    print("No router specified for subnet.")
            else:
                success, dn = create_network(
                    schoolNr,
                    network,
                    iprange=iprange,
                    defaultrouter=defaultrouter,
                    nameserver=nameserver,
                    netbiosserver=netbiosserver,
                )


def set_inventory_number_for_computer():
    # usage
    usage = "%prog [options] <importFile>\n"
    usage += "  modifies computer inventory number according to import file for UCS@school\n\n"
    usage += "Arguments:\n"
    usage += "  <importFile> computer import file"
    parser = optparse.OptionParser(usage=usage)
    options, args = parser.parse_args()

    # argument is infile
    if len(args) > 0:
        infile = args[0]
    else:
        parser.print_help()
        print("must have one Argument for infile")
        sys.exit()

    print("infile is  : " + infile)
    problem_hints = ""

    with open(infile) as inf, stopped_notifier():
        for line in inf:
            if emptyLineRe.match(line):
                continue

            parsed = line.strip("\r\n").split(sepchar)
            ctype = parsed[0]
            name = parsed[1]
            MAC = parsed[2]
            inventoryNumbers = parsed[3].strip().split(",")

            dn = get_computer_dn(name)
            if not dn:
                print("Computer to modify not found: %s" % name)
                problem_hints = problem_hints + "Computer to modify not found: %s" % name
            else:
                try:
                    computerModule = univention.admin.modules.get("computers/%s" % ctype)
                    position_basedn = univention.admin.uldap.position(baseDN)
                    univention.admin.modules.init(lo, position_basedn, computerModule)
                except Exception:  # FIXME
                    print("failed to get module of type computers/%s" % ctype)
                    traceback.print_exc(100, sys.stdout)
                    problem_hints = problem_hints + "failed to get module of type computers/%s" % ctype
                    continue
                computerObject = univention.admin.objects.get(
                    computerModule, None, lo, position="", dn=dn
                )
                computerObject.open()
                if MAC.replace("-", ":").replace(" ", ":").lower() in computerObject["mac"]:
                    try:
                        computerObject["inventoryNumber"] = inventoryNumbers
                        computerObject.modify()
                        print("modified inventory Number for %s" % dn)
                    except Exception:  # FIXME
                        print("failed to modify computer %s" % name)
                        traceback.print_exc(100, sys.stdout)
                        problem_hints = problem_hints + "failed to modify computer %s" % name
                else:
                    print("mac address does not match while modifying %s" % name)
                    problem_hints = (
                        problem_hints + "mac address does not match while modifying %s" % name
                    )


def set_policy_for_dhcp_subnet(
    network, schoolNr, policy_module, policy_dn, cn, values=None, overwrite_policy=False
):
    """
    @paramnetwork   network where policy object shall be added
    @paramschoolNrschool number
    """
    dn = None
    success = True
    if values is None:
        values = {}

    # check if routing policy needs to be created
    dhcp_subnet_object = None
    dhcp_service_list = univention.admin.modules.lookup(
        dhcp_service_module, None, lo, scope="sub", base=getDN(schoolNr)
    )
    for dhcp_service_dn in dhcp_service_list:
        dhcp_subnet_list = univention.admin.modules.lookup(
            dhcp_subnet_module,
            None,
            lo,
            scope="sub",
            superordinate=dhcp_service_dn,
            base=baseDN,
            filter=filter_format("cn=%s", [str(network.network_address)]),
        )

        subnet_dn = "cn=%s,cn=%s,cn=dhcp,%s" % (
            escape_dn_chars(str(network.network_address)),
            escape_dn_chars(schoolNr.lower()),
            getDN(schoolNr),
        )
        for dhcp_subnet in dhcp_subnet_list:
            if dhcp_subnet.dn == subnet_dn:
                dhcp_subnet_object = dhcp_subnet

    if not dhcp_subnet_object:
        print(
            "ERROR: Unable to set policy for dhcp subnet because it does not exist: %s" % (dhcp_subnet,)
        )
        return (False, dn)

    objects = univention.admin.modules.lookup(
        policy_module,
        None,
        lo,
        scope="sub",
        superordinate=superordinate,
        base=baseDN,
        filter=filter_format("cn=%s", [cn]),
    )
    policy_object = None
    if objects:
        for o in objects:
            if o.dn == policy_dn:
                policy_object = o
    if not policy_object:
        position_dn = lo.parentDn(policy_dn)
        verify_containers(position_dn, cn_module, None, lo, superordinate, baseDN, path="policyPath")
        position.setDn(position_dn)
        policy_object = policy_module.object(None, lo, position=position, superordinate=superordinate)
        policy_object.open()
        for k, v in values.items():
            policy_object[k] = v
        try:
            create_object(policy_object)
        except Exception:  # FIXME
            print("WARNING: Error creating policy %s" % policy_object.dn)
            traceback.print_exc(100, sys.stdout)
            policy_object = None
            success = False
    elif overwrite_policy:
        policy_object.open()
        for k, v in values.items():
            policy_object[k] = v
        try:
            policy_object.modify()
        except Exception:  # FIXME
            print("WARNING: Error modifying policy %s" % policy_object.dn)
            traceback.print_exc(100, sys.stdout)
            policy_object = None

    if policy_object:
        # add policy reference to dhcp_subnet_object
        dn = policy_object.dn
        try:
            lob = lo.get(dhcp_subnet_object.dn, ["objectClass"])
            if b"univentionPolicyReference" not in lob.get("objectClass", []):
                lo.modify(dhcp_subnet_object.dn, [("objectClass", b"", b"univentionPolicyReference")])

            lob = lo.get(dhcp_subnet_object.dn, ["univentionPolicyReference"])
            if policy_object.dn.encode("UTF-8") not in lob.get("univentionPolicyReference", []):
                print(
                    "connecting dhcp subnet (%s) with policy (%s)"
                    % (
                        dhcp_subnet_object.dn,
                        policy_object.dn,
                    )
                )
                lo.modify(
                    dhcp_subnet_object.dn,
                    [("univentionPolicyReference", b"", policy_object.dn.encode("utf-8"))],
                )
        except Exception:  # FIXME
            print("WARNING: Error modifying policy %s" % policy_object.dn)
            traceback.print_exc(100, sys.stdout)
            policy_object = None
            success = False

    return (success, dn)


def set_netbiosserver_for_subnet(network, ip, schoolNr, overwrite_policy=False):
    """
    @param  ip  ipaddress object for the netbios server
    @param  schoolNr    school number

    The Netbios node type is set to 8 H-node: Hybrid - WINS, then broadcast
    """
    values = {
        "name": "%s-%s" % (schoolNr.lower(), network.network_address),
        "netbios_node_type": "8",
        "netbios_name_servers": str(ip),
    }
    dn = "cn=%s,cn=netbios,cn=dhcp,cn=policies,%s" % (escape_dn_chars(values["name"]), getDN(schoolNr))
    return set_policy_for_dhcp_subnet(
        network,
        schoolNr,
        policy_dhcp_netbios_module,
        dn,
        values["name"],
        values=values,
        overwrite_policy=overwrite_policy,
    )


def set_nameserver_for_subnet(network, ip, schoolNr, overwrite_policy=False):
    """
    @param  ip  ipaddress object for the netbios server
    @param  schoolNr    school number
    """
    values = {
        "name": "%s-%s" % (schoolNr.lower(), network.network_address),
        "domain_name": domainname,
        "domain_name_servers": str(ip),
    }
    dn = "cn=%s,cn=dns,cn=dhcp,cn=policies,%s" % (escape_dn_chars(values["name"]), getDN(schoolNr))
    return set_policy_for_dhcp_subnet(
        network,
        schoolNr,
        policy_dhcp_dns_module,
        dn,
        values["name"],
        values=values,
        overwrite_policy=overwrite_policy,
    )


def set_router_for_subnet(network, ip, schoolNr, overwrite_policy=False):
    """
    @param  ip  ipaddress object for the router
    @param  schoolNr    school number
    """
    values = {"name": "%s-%s" % (schoolNr.lower(), network.network_address), "routers": str(ip)}
    dn = "cn=%s,cn=routing,cn=dhcp,cn=policies,%s" % (escape_dn_chars(values["name"]), getDN(schoolNr))
    return set_policy_for_dhcp_subnet(
        network,
        schoolNr,
        policy_dhcp_routing_module,
        dn,
        values["name"],
        values=values,
        overwrite_policy=overwrite_policy,
    )


def import_router():
    """
    No computer object is created on importing routers! But the dhcp-routing
    policy is set to the IP address of the router
    """
    import_networks(router_only=True)


def import_group():
    # usage
    usage = "%prog [options] <importFile>\n"
    usage += "  creates/modifies/deletes group objects accoring to import file for UCS@school\n\n"
    usage += "Arguments:\n"
    usage += "  <importFile> group import file"
    parser = optparse.OptionParser(usage=usage)
    options, args = parser.parse_args()

    # argument is infile
    if len(args) > 0:
        infile = args[0]
    else:
        parser.print_help()
        print("must have one Argument for infile")
        sys.exit()

    print("infile is  : " + infile)
    inf = open(infile)

    with stopped_notifier():
        for line in inf:
            if emptyLineRe.match(line):
                continue

            parsed = line.strip("\r\n").split(sepchar)

            action = parsed[0]  # action=A(dd, M(odify, D(elete
            schoolNr = parsed[1]
            ClassID = parsed[2]
            Descrpt = parsed[3]

            group_dn = "cn=%s,cn=klassen,cn=%s,cn=groups,%s" % (
                escape_dn_chars(ClassID),
                escape_dn_chars(cn_pupils),
                getDN(schoolNr),
            )
            share_dn = "cn=%s,cn=klassen,cn=shares,%s" % (escape_dn_chars(ClassID), getDN(schoolNr))

            verify_school_ou(schoolNr, lo)

            if action in ["A", "M"]:  # Add/Modify group
                # verify_group(group_dn, None, lo, superordinate, baseDN, schoolNr)
                # object = univention.admin.objects.get(group_module, None, lo, position='', dn=group_dn)
                # object.open()
                # object['description']=Descrpt
                # object.modify()
                # verify_group_share(schoolNr, ClassID, None, lo, superordinate, baseDN, schoolNr)

                # elif action == 'M':  # M)odify group
                verify_group(group_dn, None, lo, superordinate, baseDN, schoolNr, is_school_class=True)
                object = univention.admin.objects.get(group_module, None, lo, position="", dn=group_dn)
                object.open()
                oldDescrpt = object["description"]
                object["description"] = Descrpt
                print("Changed Description of %s from %s to %s" % (group_dn, oldDescrpt, Descrpt))
                object.modify()
                verify_group_share(schoolNr, ClassID, None, lo, superordinate, baseDN)
            elif action == "D":  # D)elete group
                delete_dn(group_dn, group_module, None, lo)
                delete_dn(share_dn, share_module, None, lo)


def import_printer():
    # usage
    usage = "%prog [options] <importFile>\n"
    usage += "  creates/modifies/deletes printer objects according to import file for UCS@school\n\n"
    usage += "Arguments:\n"
    usage += "  <importFile> printer import file"
    parser = optparse.OptionParser(usage=usage)
    options, args = parser.parse_args()

    # argument is infile
    if len(args) > 0:
        infile = args[0]
    else:
        parser.print_help()
        print("must have one Argument for infile")
        sys.exit()

    print("infile is  : " + infile)
    with open(infile) as inf, stopped_notifier():
        for line in inf:
            if emptyLineRe.match(line):
                continue

            parsed = line.strip("\r\n").split(sepchar)

            # completely new
            action = parsed[0]  # action=A(dd, M(odify, D(elete
            schoolNr = parsed[1]
            spoolHost = parsed[2].lower()
            if "." not in spoolHost:
                spoolHost = "%s.%s" % (spoolHost, ucr["domainname"])
            printerName = parsed[3]
            printerUri = parsed[4]
            verify_school_ou(schoolNr, lo)

            def parseString(parsed, idx, default=None):
                res = default
                if len(parsed) > idx:
                    string = parsed[idx]
                    if string:
                        res = string
                return res

            printerModel = parseString(parsed, 5)

            position_dn = "cn=%s,cn=printers,%s" % (escape_dn_chars(printerName), getDN(schoolNr))
            container = "cn=printers,%s" % (getDN(schoolNr),)

            if action in ["A", "M"]:  # Add/Modify printer
                position = univention.admin.uldap.position(baseDN)
                position.setDn(container)
                if action == "A":
                    object = printer_module.object(None, lo, position, superordinate=superordinate)
                else:
                    object = univention.admin.objects.get(
                        printer_module, None, lo, position="", dn=position_dn
                    )
                object.open()
                object["name"] = printerName
                object["spoolHost"] = [spoolHost]
                object["uri"] = printer_module.unmapPrinterURI([printerUri.encode("UTF-8")])
                if printerModel:
                    object["model"] = printerModel
                else:
                    object["model"] = "None"
                if action == "A":
                    create_object(object)
                else:
                    print("modifying object %s" % position_dn)
                    object.modify()
            elif action == "D":  # D)elete printer
                delete_dn(position_dn, printer_module, None, lo)


def activate_groupmembers():
    #  syntax: $0 <groupXXX> [0|1] [0|1]"
    # <groupXXX>    group
    # [0|1]         optional: deactivate | activate
    # [0|1]         optional: keep passwords | set random passwords

    chpasswd = 0
    status = None

    # usage
    usage = "%prog [options] <groupName> <newStatus> <changePassword>\n"
    usage += "  (de)activates group members for UCS@school\n\n"
    usage += "Arguments:\n"
    usage += "  <groupName>      group that shall be dis-/enabled\n"
    usage += "  <newStatus>      0=disabled, 1=enabled\n"
    usage += "  <changePassword> 0=keep passwords, 1=set random passwords\n"
    usage += "                   the logfile groupName.csv contains all activated users"
    parser = optparse.OptionParser(usage=usage)
    options, args = parser.parse_args()

    if len(args) < 2:
        parser.print_help()
        print("must have at least one argument")
        sys.exit()
    if len(args) > 0:
        actgrp = args[0]
    if len(args) > 1:
        try:
            status = int(args[1])
        except ValueError:
            parser.print_help()
            print("invalid value for <newstatus>!")
            sys.exit()
    if len(args) > 2:
        try:
            chpasswd = int(args[2])
        except ValueError:
            parser.print_help()
            print("invalid value for <changePassword>!")
            sys.exit()

    if status not in [0, 1]:
        parser.print_help()
        print("invalid value for <newstatus>!")
        sys.exit()
    elif chpasswd not in [0, 1]:
        parser.print_help()
        print("invalid value for <changePassword>!")
        sys.exit()

    # inverted logic
    if status == 0:
        disabled = "1"
    else:
        disabled = "0"

    # outfile = os.path.basename(actgrp+'.csv')
    outfile = "/var/lib/ucs-school-import/activate-grp-%s-%s.csv" % (
        time.strftime("%Y%m%d-%H%M%S"),
        actgrp,
    )
    print("outfile is : " + outfile)
    if os.path.exists(outfile):
        print("outfile %s does already exist. Stopping here." % outfile)
        sys.exit(1)

    with open(outfile, "w") as outf:
        # get group
        objects = univention.admin.modules.lookup(
            group_module,
            None,
            lo,
            scope="sub",
            superordinate=superordinate,
            base=baseDN,
            filter=filter_format("cn=%s", [actgrp]),
        )
        out_line = ("%%s%(sep)s" * 8 + "\n") % {"sep": sepchar}
        for grp in objects:
            # get group members
            if "uniqueMember" in grp.oldattr:
                grpmembers = grp.oldattr["uniqueMember"]
                for memberdn in grpmembers:
                    memberdn = memberdn.decode("UTF-8")

                    membergrplist = ""
                    grpobjlist = univention.admin.modules.lookup(
                        group_module,
                        None,
                        lo,
                        scope="sub",
                        superordinate=superordinate,
                        base=baseDN,
                        filter=filter_format("uniqueMember=%s", [memberdn]),
                    )
                    for grpobj in grpobjlist:
                        if grpobj.dn != grp.dn:
                            if membergrplist:
                                membergrplist += "," + grpobj.oldattr["cn"][0].decode("UTF-8")
                            else:
                                membergrplist += grpobj.oldattr["cn"][0].decode("UTF-8")

                    # modify each member
                    memberobj = univention.admin.objects.get(
                        user_module, None, lo, position="", dn=memberdn
                    )
                    memberobj.open()

                    memberobj["disabled"] = disabled

                    print(memberdn, end=" ")
                    if status == 0:
                        print("deactivated", end=" ")
                    else:
                        print("activated", end=" ")

                    passwd = "*"  # nosec
                    if chpasswd:
                        passwd = create_passwd(dn=memberdn, specials="-+.,;")
                        memberobj["password"] = passwd
                        memberobj["overridePWHistory"] = "1"
                        memberobj["overridePWLength"] = "1"
                        print("and set random password", end=" ")

                    memberobj.modify()

                    # write csv line
                    snr = next(x[0][1] for x in str2dn(memberdn) if x[0][0] == "ou")
                    outf.write(
                        out_line
                        % (
                            snr,
                            actgrp,
                            memberobj["username"],
                            memberobj["firstname"],
                            memberobj["lastname"],
                            passwd,
                            memberobj["mailPrimaryAddress"],
                            membergrplist,
                        )
                    )

                    print()


def export_computer():
    #  syntax: $0 <ou> <filename>"
    # <ou>        computers of OU <ou> shall be exported
    # <filename>  output filename

    # usage
    usage = "%prog [options] <ouName> <fileName>\n"
    usage += "  export computer for specified ou in UCS@school\n\n"
    usage += "Arguments:\n"
    usage += "  <ouName>         name of the ou from which the computers shall be exported\n"
    usage += "  <fileName>       export file"
    parser = optparse.OptionParser(usage=usage)
    options, args = parser.parse_args()

    if len(args) != 2:
        parser.print_help()
        print("must have at least two arguments")
        sys.exit()
    ou = args[0]
    outfile = args[1]

    verify_school_ou(ou, lo)
    # outfile = os.path.basename(actgrp+'.csv')
    print("outfile is : " + outfile)
    if os.path.exists(outfile):
        print("outfile %s does already exist. Stopping here." % outfile)
        sys.exit(1)
    outf = open(outfile, "w")

    ouBaseDN = getDN(ou)

    # get group
    print("looking for objects... please wait...")
    objects = univention.admin.modules.lookup(
        computer_module, None, lo, scope="sub", superordinate=superordinate, base=ouBaseDN
    )
    print("writing data of %d objects..." % len(objects))
    out_line = ("%%s%(sep)s" * 5 + "\n") % {"sep": sepchar}
    for host in objects:
        host.open()
        inventoryNumber = ""
        ip = ""
        mac = ""
        if host.get("inventoryNumber"):
            inventoryNumber = host["inventoryNumber"][0]
        if host.get("ip"):
            ip = host["ip"][0]
        if host.get("mac"):
            mac = host["mac"][0]
        # Name,OU,1.Zeile Inventar-Nr.,IP-Adresse
        outf.write(out_line % (host["name"], ou, inventoryNumber, ip, mac))
    outf.close()


def set_ou_sharefileserver():
    # usage
    usage = "%prog [options] <ouName> <shareFileServer>\n"
    usage += "  sets shareFileServer for ou in UCS@school\n\n"
    usage += "Arguments:\n"
    usage += "  <ouName>          name of the ou\n"
    usage += "  <shareFileServer> name of the shareFileServer"
    parser = optparse.OptionParser(usage=usage)
    options, args = parser.parse_args()

    if len(args) != 2:
        parser.print_help()
        print("ouname and ShareFileServer is required")
        sys.exit(1)
    ouname = args[0]
    ShareFileServer = args[1]

    if not re.match(r"^[a-zA-Z0-9](([a-zA-Z0-9-_]*)([a-zA-Z0-9]$))?$", ouname):
        print("ERROR: invalid ouname name given")
        sys.exit(1)
    if not re.match(r"^[a-zA-Z0-9](([a-zA-Z0-9-_]*)([a-zA-Z0-9]$))?$", ShareFileServer):
        print("ERROR: invalid ShareFileServer name given")
        sys.exit(1)
    objects = lo.searchDn(
        filter=filter_format("(&(objectClass=univentionHost)(cn=%s))", [ShareFileServer]), base=baseDN
    )
    if len(objects) != 1:
        print('ERROR: sharefileserver "%s" not found!' % ShareFileServer)
        sys.exit(1)
    else:
        ShareFileServer = objects[0]

    verify_school_ou(ouname, lo)

    with stopped_notifier():
        r = lo.get(getDN(ouname), ["objectClass"])
        if b"ucsschoolOrganizationalUnit" not in r["objectClass"]:
            r = lo.modify(
                getDN(ouname),
                [
                    ("objectClass", b"", b"ucsschoolOrganizationalUnit"),
                    ("ucsschoolClassShareFileServer", b"", ShareFileServer.encode("UTF-8")),
                ],
            )
        else:
            r = lo.get(getDN(ouname), ["ucsschoolClassShareFileServer"])
            if r:
                r = r["ucsschoolClassShareFileServer"][0]
            else:
                r = b""
            r = lo.modify(
                getDN(ouname), [("ucsschoolClassShareFileServer", r, ShareFileServer.encode("UTF-8"))]
            )
    sys.exit(0)


def rename_class():
    # usage
    usage = "%prog [options] <importFile>\n"
    usage += "  renames class groups and shares according to import file for UCS@school\n\n"
    usage += "Arguments:\n"
    usage += "  <importFile> rename class import file"
    parser = optparse.OptionParser(usage=usage)
    options, args = parser.parse_args()

    # argument is infile
    if len(args) > 0:
        infile = args[0]
    else:
        parser.print_help()
        print("must have one Argument for infile")
        sys.exit(1)

    # get all groups and shares
    myGroups = {}
    myShares = {}
    for group in univention.admin.modules.lookup(
        group_module,
        None,
        lo,
        scope="sub",
        superordinate=superordinate,
        base=baseDN,
        filter="(objectClass=univentionGroup)",
    ):
        cn = str2dn(group.dn)[0][0][1]
        myGroups[cn] = group
    for share in univention.admin.modules.lookup(
        share_module,
        None,
        lo,
        scope="sub",
        superordinate=superordinate,
        base=baseDN,
        filter="(objectClass=univentionShare)",
    ):
        cn = str2dn(share.dn)[0][0][1]
        myShares[cn] = share

    print("infile is  : " + infile)
    inf = open(infile)

    # get old and new group name from infile
    newGroups = []
    lineCounter = 0
    for line in inf:
        lineCounter += 1
        if emptyLineRe.match(line):
            continue
        parsed = line.strip("\r\n").split(sepchar)

        # save old and new name and the udm share and group
        # object
        groupObject = myGroups.get(parsed[0], None)
        shareObject = myShares.get(parsed[0], None)
        newGroups.append(
            {
                "oldName": parsed[0],
                "newName": parsed[1],
                "share": shareObject,
                "group": groupObject,
                "line": line.strip(),
                "lineCounter": "%s" % lineCounter,
            }
        )

    # check groups
    error = ""
    for i in newGroups:
        oldName = i.get("oldName")
        newName = i.get("newName")
        line = i.get("line")
        lineCounter = i.get("lineCounter")
        ud.debug(
            ud.MAIN,
            ud.PROCESS,
            "Checking line %s: %r to %r" % (lineCounter, oldName, newName),
        )

        # groups
        if not myGroups.get(oldName):
            error = "old group %s not found" % oldName
        elif myGroups.get(newName):
            error = "new group %s already exists" % newName
        else:
            del myGroups[oldName]
            myGroups[newName] = 1

        # shares
        if not myShares.get(oldName):
            error = "old share %s not found" % oldName
        elif myShares.get(newName):
            error = "new share %s already exists" % newName
        else:
            del myShares[oldName]
            myShares[newName] = 1

        if error:
            ud.debug(
                ud.MAIN,
                ud.ERROR,
                "%s (line %s: %s)" % (error, lineCounter, line),
            )
            ud.debug(ud.MAIN, ud.ERROR, "Stopping here")
            print("ERROR: %s (line %s: %s)" % (error, lineCounter, line))
            print("do nothing")
            sys.exit(1)

    ud.debug(ud.MAIN, ud.PROCESS, "Stopping notifier")
    with stopped_notifier():
        # do it, rename groups and shares and modify share path
        messages = []
        for i in newGroups:
            groupObject = i.get("group")
            shareObject = i.get("share")
            newName = i.get("newName")
            oldName = i.get("oldName")
            line = i.get("line")
            lineCounter = i.get("lineCounter")
            if groupObject and shareObject and newName and oldName:
                ud.debug(
                    ud.MAIN,
                    ud.PROCESS,
                    "Processing line %s: renaming %r to %r" % (lineCounter, oldName, newName),
                )

                success = True

                # rename group
                groupObject.open()
                groupObject["name"] = newName
                try:
                    ud.debug(
                        ud.MAIN,
                        ud.PROCESS,
                        "Renaming of group %r to %r" % (oldName, newName),
                    )
                    groupObject.modify()
                    messages.append(
                        (False, "Renaming of group %s to %s successfully" % (oldName, newName))
                    )
                except Exception as e:
                    success = False
                    messages.append(
                        (True, "Renaming of group %s to %s failed: %s" % (oldName, newName, str(e)))
                    )

                if success:
                    # rename share and share path
                    shareObject.open()
                    path = shareObject["path"].split(os.path.sep)
                    path[-1] = newName
                    shareObject["name"] = newName
                    if shareObject["sambaName"] == oldName:
                        shareObject["sambaName"] = newName
                    if shareObject["sambaForceGroup"] == "+%s" % (oldName,):
                        shareObject["sambaForceGroup"] = "+%s" % (newName,)
                    shareObject["path"] = os.path.sep.join(path)
                    try:
                        ud.debug(
                            ud.MAIN,
                            ud.PROCESS,
                            "Renaming of share %r to %r" % (oldName, newName),
                        )
                        shareObject.modify()
                        messages.append(
                            (False, "Renaming of share %s to %s successfully" % (oldName, newName))
                        )
                    except Exception as e:
                        success = False
                        messages.append(
                            (True, "Renaming of share %s to %s failed: %s" % (oldName, newName, str(e)))
                        )

    ud.debug(ud.MAIN, ud.PROCESS, "Resuming notifier")

    # print warnings
    for isError, msg in messages:
        level = {False: ud.PROCESS, True: ud.ERROR}[isError]
        ud.debug(ud.MAIN, level, msg)
        print(msg)

    ud.debug(ud.MAIN, ud.PROCESS, "rename_class finished")


def move_domaincontroller_to_ou(co, lo, baseDN, options):
    """Move specified domaincontroller (options.dcname) to specified OU (options.ou)."""
    slaves = univention.admin.modules.lookup(
        server_module,
        None,
        lo,
        scope="sub",
        base=baseDN,
        filter=filter_format("cn=%s", [options.dcname]),
    )
    if not slaves:
        print('ERROR: cannot find Replica Directory Node with hostname "%s"' % options.dcname)
        sys.exit(1)
    if len(slaves) > 1:
        print('ERROR: found more than one Replica Directory Node with hostname "%s"' % options.dcname)
        sys.exit(1)

    oulist = univention.admin.modules.lookup(
        ou_module,
        None,
        lo,
        scope="sub",
        base=baseDN,
        filter=filter_format("ou=%s", [options.ou]),
    )
    if not oulist:
        print('ERROR: the given ou "%s" does not exist' % options.ou)
        sys.exit(1)
    if len(oulist) > 1:
        print('ERROR: found more than one ou "%s" in LDAP' % options.ou)
        sys.exit(1)

    slave = slaves[0]
    ouDn = oulist[0].dn

    group_filter = filter_format(
        "(&(|(cn=%s)(cn=%s))(uniqueMember=%s))",
        ["OU%s-DC-Edukativnetz" % options.ou, "OU%s-DC-Verwaltungsnetz" % options.ou, slave.dn],
    )
    groups = univention.admin.modules.lookup(
        group_module, None, lo, scope="sub", base=baseDN, filter=group_filter
    )
    if not groups:
        print(
            'ERROR: cannot move Replica Directory Node with hostname "%s" to OU "%s"'
            % (
                options.dcname,
                options.ou,
            )
        )
        print("ERROR: The system has no LDAP access to the OU")
        sys.exit(1)

    if slave.dn.endswith(",%s" % ouDn):
        print(
            'Replica Directory Node "%s" is already located below ou "%s" - stopping here'
            % (
                options.dcname,
                options.ou,
            )
        )
        sys.exit(0)

    REis_in_ou = re.compile(r"^.+,ou=[^,]+,%s$" % baseDN)
    if REis_in_ou.match(slave.dn) and not options.force:
        print('ERROR: Replica Directory Node "%s" is located in another OU' % (options.dcname))
        print("ERROR: DN of Replica Directory Node: %s" % slave.dn)
        print("ERROR: use --force to override")
        sys.exit(1)

    cnDn = "cn=dc,cn=server,cn=computers,%s" % (getDN(options.ou),)
    containers = univention.admin.modules.lookup(cn_module, None, lo, scope="base", base=cnDn)
    if not containers:
        print(
            "ERROR: cannot move Replica Directory Node object - target container does not exist: %s"
            % cnDn
        )
        sys.exit(1)

    oldDn = slave.dn
    newDn = "cn=%s,cn=dc,cn=server,cn=computers,%s" % (
        escape_dn_chars(options.dcname),
        getDN(options.ou),
    )
    slave.open()
    print("Moving %s to %s ..." % (oldDn, newDn))
    slave.move(newDn, ignore_license=True)

    oulist = univention.admin.modules.lookup(ou_module, None, lo, scope="sub", base=baseDN)
    for ou in oulist:
        ou.open()
        changed = False
        for property in ("ucsschoolClassShareFileServer", "ucsschoolHomeShareFileServer"):
            if oldDn in ou[property]:
                ou[property] = newDn
                changed = True
        if changed:
            print("Updating %s ..." % ou.dn)
            ou.modify()

    removed = False
    ou_dhcpservice = None

    # find dhcp server object by checking all dhcp service objects
    dhcpservicelist = univention.admin.modules.lookup(
        dhcp_service_module, None, lo, scope="sub", base=baseDN
    )
    for dhcpservice in dhcpservicelist:
        # remember dhcpservice of target OU
        if dhcpservice.dn.endswith(",%s" % ouDn):
            ou_dhcpservice = dhcpservice

        # find dhcp server
        serverlist = univention.admin.modules.lookup(
            dhcp_server_module,
            None,
            lo,
            scope="sub",
            superordinate=dhcpservice,
            base=baseDN,
            filter=filter_format("cn=%s", [options.dcname]),
        )
        if serverlist:
            if not serverlist[0].dn.endswith(",%s" % ouDn):
                removed = True
                print("Removing %s" % serverlist[0].dn)
                serverlist[0].open()
                attr_server = serverlist[0]["server"]
                serverlist[0].remove()

    if removed:
        position = univention.admin.uldap.position(
            "cn=%s,cn=dhcp,%s" % (escape_dn_chars(options.ou), getDN(options.ou))
        )
        dhcpserver = dhcp_server_module.object(None, lo, position=position, superordinate=ou_dhcpservice)
        dhcpserver.open()
        dhcpserver["server"] = attr_server
        create_object(dhcpserver)

    print("Move complete")
    print("HINT: the Replica Directory Node has to be rejoined into the domain!")


def create_dc(co, lo, baseDN, ou_name, dc_name, dc_type):
    if dc_type not in (TYPE_DC_EDUCATIONAL, TYPE_DC_ADMINISTRATIVE):
        print("ERROR: invalid type for domaincontroller given in create_dc(): %r" % (dc_type,))
        sys.exit(1)

    if dc_type == TYPE_DC_ADMINISTRATIVE and not ucr.is_true(
        "ucsschool/ldap/noneducational/create/objects", True
    ):
        print(
            "ERROR: cannot create administrative DC - noneducational objects disabled via ucsschool/ldap/noneducational/create/objects"  # noqa: E501
        )
        sys.exit(1)

    if not dc_name or not is_valid_hostname(dc_name):
        print("ERROR: invalid administrative server name given: %r" % dc_name)
        sys.exit(1)

    # verify if OU already exists - abort otherwise
    ou_objects = univention.admin.modules.lookup(
        ou_module,
        None,
        lo,
        scope="sub",
        superordinate=superordinate,
        base=baseDN,
        filter=filter_format("ou=%s", [ou_name]),
    )
    if not ou_objects:
        print("ERROR: specified OU %r does not exist" % ou_name)
        sys.exit(1)

    # get list of desired group memberships
    group_dn_list = {
        TYPE_DC_ADMINISTRATIVE: [
            "cn=OU%s-DC-Verwaltungsnetz,cn=ucsschool,cn=groups,%s"
            % (escape_dn_chars(ou_name.lower()), baseDN),
            "cn=DC-Verwaltungsnetz,cn=ucsschool,cn=groups,%s" % (baseDN,),
        ],
        TYPE_DC_EDUCATIONAL: [
            "cn=DC-Edukativnetz,cn=ucsschool,cn=groups,%s" % (baseDN,),
            "cn=OU%s-DC-Edukativnetz,cn=ucsschool,cn=groups,%s"
            % (escape_dn_chars(ou_name.lower()), baseDN),
        ],
    }[dc_type]
    for grpdn in group_dn_list:
        verify_group(grpdn, None, lo, superordinate, baseDN, ou_name)

    host_objects = univention.admin.modules.lookup(
        computer_module,
        None,
        lo,
        scope="sub",
        superordinate=superordinate,
        base=baseDN,
        filter=filter_format("cn=%s", [dc_name]),
    )
    slave_objects = univention.admin.modules.lookup(
        server_module,
        None,
        lo,
        scope="sub",
        superordinate=superordinate,
        base=baseDN,
        filter=filter_format("cn=%s", [dc_name]),
    )
    if host_objects and not slave_objects:
        print(
            'ERROR: given host name "%s" is already in use and no Replica Directory Node system. Please choose another name.'  # noqa: E501
            % dc_name
        )
        sys.exit(1)

    if slave_objects:
        # slave object does exist
        if len(slave_objects) > 1:
            print(
                "ERROR: more than one Replica Directory Node with cn=%s found in LDAP: %r"
                % (
                    dc_name,
                    slave_objects,
                )
            )
            sys.exit(1)

        slave_object = slave_objects[0]
        slave_object.open()
        modified = False
        for grpdn in group_dn_list:
            if grpdn not in slave_object["groups"]:
                slave_object["groups"].append(grpdn)
                modified = True
        if modified:
            slave_object.modify()
            print("modifed object:", slave_object.dn)
    else:
        # Replica Directory Node object does NOT exist - create missing DC object
        position = univention.admin.uldap.position(baseDN)
        position.setDn("cn=dc,cn=server,cn=computers,%s" % getDN(ou_name))
        slave_object = server_module.object(None, lo, position=position, superordinate=superordinate)
        slave_object.open()
        slave_object["name"] = dc_name
        slave_object["unixhome"] = "/dev/null"
        slave_object["shell"] = "/bin/bash"
        slave_object["primaryGroup"] = "cn=DC Slave Hosts,cn=groups,%s" % (baseDN,)
        if dc_type == TYPE_DC_ADMINISTRATIVE:
            roles = [create_ucsschool_role_string(role_dc_slave_admin, ou_name)]
        else:
            roles = [create_ucsschool_role_string(role_dc_slave_edu, ou_name)]
        slave_object["ucsschoolRole"] = roles
        for grpdn in group_dn_list:
            if grpdn not in slave_object["groups"]:
                slave_object["groups"].append(grpdn)
        create_object(slave_object)


# ------------------------------------------------------------------------------------------#
# MAIN                                                                                     #
# ------------------------------------------------------------------------------------------#

# init univention-directory-manager
try:
    lo, position = univention.admin.uldap.getAdminConnection()
except Exception as e:
    ud.debug(ud.MAIN, ud.WARN, "authentication error: %s" % str(e))
    print("ERROR: authentication error: %s" % str(e))
    sys.exit(1)

superordinate = None

univention.admin.modules.update()
user_module = univention.admin.modules.get("users/user")
group_module = univention.admin.modules.get("groups/group")
cn_module = univention.admin.modules.get("container/cn")
ou_module = univention.admin.modules.get("container/ou")
dcmaster_module = univention.admin.modules.get("computers/domaincontroller_master")
dcbackup_module = univention.admin.modules.get("computers/domaincontroller_backup")
server_module = univention.admin.modules.get("computers/domaincontroller_slave")
share_module = univention.admin.modules.get("shares/share")
printer_module = univention.admin.modules.get("shares/printer")
computer_module = univention.admin.modules.get("computers/computer")
dhcp_server_module = univention.admin.modules.get("dhcp/server")
dhcp_service_module = univention.admin.modules.get("dhcp/service")
dhcp_subnet_module = univention.admin.modules.get("dhcp/subnet")
dns_reverse_zone_module = univention.admin.modules.get("dns/reverse_zone")
network_module = univention.admin.modules.get("networks/network")
policy_dhcp_routing_module = univention.admin.modules.get("policies/dhcp_routing")
policy_dhcp_netbios_module = univention.admin.modules.get("policies/dhcp_netbios")
policy_dhcp_dns_module = univention.admin.modules.get("policies/dhcp_dns")
setting_module = univention.admin.modules.get("settings/default")
maildomain_module = univention.admin.modules.get("mail/domain")

for m in (
    user_module,
    group_module,
    cn_module,
    ou_module,
    server_module,
    share_module,
    printer_module,
    computer_module,
    dhcp_server_module,
    dhcp_service_module,
    dhcp_subnet_module,
    dns_reverse_zone_module,
    network_module,
    policy_dhcp_routing_module,
    policy_dhcp_netbios_module,
    policy_dhcp_dns_module,
    setting_module,
    maildomain_module,
):
    univention.admin.modules.init(lo, position, m)

# get mail domains
for result in lo.search(base=baseDN, filter="(objectClass=univentionMailDomainname)"):
    cn = result[1].get("cn", [b""])[0].decode("UTF-8")
    if cn:
        mailDomains[cn] = 1

# get default group
default_group = "cn=Domain Users,cn=groups,%s" % baseDN  # fallback
for object in univention.admin.modules.lookup(
    setting_module, None, lo, scope="sub", superordinate=superordinate, base="", filter=""
):
    if object.has_property("defaultGroup"):
        default_group = object["defaultGroup"]
    else:
        print("WARNING, no default group found")

ud.debug(
    ud.MAIN,
    ud.PROCESS,
    "Starting %s %s" % (os.path.basename(sys.argv[0]), " ".join([repr(x) for x in sys.argv[1:]])),
)

# select action
if sys.argv[0].endswith("import_networks"):
    import_networks()
elif sys.argv[0].endswith("import_router"):
    import_router()
elif sys.argv[0].endswith("import_inventory_number"):
    set_inventory_number_for_computer()
elif sys.argv[0].endswith("import_group"):
    import_group()
elif sys.argv[0].endswith("import_printer"):
    import_printer()
elif sys.argv[0].endswith("create_dc"):
    usage = """%prog --ou <ou> --name <dcname> --type <dctype>

%prog creates Replica Directory Node objects within the given OU."""

    parser = optparse.OptionParser(usage=usage)
    parser.add_option(
        "--name",
        action="store",
        dest="dc_name",
        default=None,
        help="hostname of the Replica Directory Node object that shall be created",
    )
    parser.add_option(
        "--ou",
        action="store",
        dest="ou_name",
        default=None,
        help="target OU where the Replica Directory Node object will be created in",
    )
    parser.add_option(
        "--type",
        action="store",
        dest="dc_type",
        default=None,
        help="UCS@school type of the Replica Directory Node object (valid values: %r, %r)"
        % (TYPE_DC_EDUCATIONAL, TYPE_DC_ADMINISTRATIVE),
    )
    options, args = parser.parse_args()

    if not options.dc_name or not options.ou_name:
        print("ERROR: no domaincontroller hostname or ou specified")
        print()
        parser.print_help()
        sys.exit(1)

    if not is_valid_ou_name(options.ou_name):
        print("ERROR: invalid ou name given")
        print()
        parser.print_help()
        sys.exit(1)

    verify_school_ou(options.ou_name, lo)

    if not is_valid_hostname(options.dc_name):
        print("ERROR: invalid domaincontroller name given")
        print()
        parser.print_help()
        sys.exit(1)

    if options.dc_type not in (TYPE_DC_EDUCATIONAL, TYPE_DC_ADMINISTRATIVE):
        print("ERROR: invalid type specified")
        print()
        parser.print_help()
        sys.exit(1)

    with stopped_notifier():
        create_dc(None, lo, baseDN, options.ou_name, options.dc_name, options.dc_type)

elif sys.argv[0].endswith("move_domaincontroller_to_ou"):
    usage = """%prog --dcname <dcname> --ou <ou>

%prog moves Replica Directory Node objects that are not located
within an OU LDAP subtree to the given OU."""

    parser = optparse.OptionParser(usage=usage)
    parser.add_option(
        "--dcname",
        action="store",
        dest="dcname",
        default=None,
        help="hostname of the Replica Directory Node object that shall be moved",
    )
    parser.add_option(
        "--ou",
        action="store",
        dest="ou",
        default=None,
        help="target OU where the Replica Directory Node object is moved to",
    )
    parser.add_option(
        "--force",
        action="store_true",
        dest="force",
        default=False,
        help="force move of Replica Directory Node object if located in other OU",
    )
    options, args = parser.parse_args()

    if not options.dcname or not options.ou:
        print("ERROR: no domaincontroller hostname or ou specified")
        print()
        parser.print_help()
        sys.exit(1)

    if not is_valid_ou_name(options.ou):
        print("ERROR: invalid ou name given")
        print()
        parser.print_help()
        sys.exit(1)

    verify_school_ou(options.ou, lo)

    if not is_valid_hostname(options.dcname):
        print("ERROR: invalid domaincontroller name given")
        print()
        parser.print_help()
        sys.exit(1)

    with stopped_notifier():
        move_domaincontroller_to_ou(None, lo, baseDN, options)

elif sys.argv[0].endswith("activate_groupmembers"):
    activate_groupmembers()
elif sys.argv[0].endswith("export_computer"):
    export_computer()
elif sys.argv[0].endswith("set_ou_sharefileserver"):
    set_ou_sharefileserver()
elif sys.argv[0].endswith("rename_class"):
    rename_class()
else:
    print("unknown action defined by the filename")
    sys.exit(1)
