#!/usr/share/ucs-test/runner python3
## desc: Test Window client join into UCS@school S4 Replica Directory Node.
## bugs: [34219]
## roles:
## - domaincontroller_slave
## packages: [univention-samba4]
## tags: [apptest,ucsschool,ucsschool_base1]
## exposure: dangerous
## versions:
##  4.0-0: skip
##  3.2-5: skip
##  4.0-1: skip
##  4.1-2: fixed

from __future__ import print_function

from multiprocessing import Process
from os import getenv
from re import IGNORECASE, search
from shutil import rmtree
from subprocess import PIPE
from tempfile import mkdtemp
from time import sleep

import workaround
from samba import generate_random_password
from samba.credentials import DONT_USE_KERBEROS, Credentials
from samba.net import LIBNET_JOIN_AUTOMATIC, Net
from samba.param import LoadParm

from univention.testing import utils
from univention.testing.strings import random_username
from univention.testing.ucsschool.test_samba4 import TestSamba4
from univention.testing.ucsschool.ucs_test_school import UCSTestSchool


class TestS4ClientJoinIntoSchool(TestSamba4):
    def __init__(self):
        """Test class constructor."""
        super(TestS4ClientJoinIntoSchool, self).__init__()

        self.domain_name = ""

        self.SambaLP = LoadParm()
        self.SambaCreds = Credentials()
        self.SambaNet = None

    def configure_samba_credentials_lp(self):
        """
        Sets credentials, creates class instances needed for a computer join.
        Creates temporary folder to use as a private directory for the test
        and returns its path.
        """
        print("\nSetting up credentials and a temporary private directory for Samba:")

        smb_conf_path = getenv("SMB_CONF_PATH")

        if smb_conf_path:
            self.SambaLP.load(smb_conf_path)
        else:
            self.SambaLP.load_default()

        private_dir = mkdtemp()
        self.SambaLP.set("private dir", private_dir)
        print("Samba temporary private dir path:", private_dir)

        self.SambaCreds.parse_string(self.admin_username)
        self.SambaCreds.set_password(self.admin_password)
        self.SambaCreds.set_domain(self.domain_name)
        self.SambaCreds.set_kerberos_state(DONT_USE_KERBEROS)
        self.SambaCreds.guess(self.SambaLP)

        self.SambaNet = Net(
            self.SambaCreds, self.SambaLP, server=self.UCR.get("interfaces/eth0/address")
        )

        return private_dir

    def join_a_computer(self, netbios_name):
        """
        Initiates a join of a computer with a given 'netbios_name' using
        Samba libnet. Returns machine password.
        """
        machine_pass = generate_random_password(20, 32)
        print(
            "\nInitiating a join of a computer with a name '%s' and a generated machine password '%s' "
            "using libnet:" % (netbios_name, machine_pass)
        )

        join_password = domain_sid = domain_name = ""
        join_password, domain_sid, domain_name = self.SambaNet.join_member(
            self.domain_name, netbios_name, LIBNET_JOIN_AUTOMATIC, machinepass=machine_pass
        )
        if not all((join_password, domain_sid, domain_name)):
            utils.fail(
                "The join password, domain SID and domain name should be returned by the libnet join, "
                "however at least one of them is empty"
            )
        utils.wait_for_replication()

        print("Join Password:", join_password)
        print("Domain SID:", domain_sid)
        print("Domain Name:", domain_name)

        return join_password

    def check_sysvol_access(self, computer_name, machine_password):
        """
        Tries to access the sysvol using 'smb-client' with given credentials
        for a created Windows Client account and lists the contents.
        """
        print(
            "\nTrying to access the Samba SYSVOL using joined Windows client credentials: computer "
            "name '%s' and machine password '%s'" % (computer_name, machine_password)
        )

        cmd = (
            "smbclient",
            "//" + self.UCR.get("ldap/server/name") + "/sysvol",
            "--user=" + computer_name + "$%" + machine_password,
            "-c",
            "ls",
        )

        print("\nExecuting command:", cmd)
        stdout, stderr = self.create_and_run_process(cmd)

        if stderr:
            print("The 'smbclient' has produced the following output to STDERR: %s" % stderr)
        if not stdout:
            utils.fail(
                "The 'smbclient' did not produce any output to STDOUT, while Samba SYSVOL contents "
                "listing were expected"
            )
        if self.domain_name not in stdout:
            utils.fail(
                "The folder with the domain name '%s' was not found in the 'smbclient' STDOUT: %s"
                % (self.domain_name, stdout)
            )

    def check_computer_via_udm(self, computer_name):
        """
        Lists the Windows computers via udm and checks the output for a given
        'computer_name'.
        """
        print(
            "\nListing all Windows computers to check the presence of the test client with a name '%s':"
            % computer_name
        )

        cmd = ("udm", "computers/windows", "list")
        print("Executing command:", cmd)
        stdout, stderr = self.create_and_run_process(cmd)

        if stderr:
            utils.fail("An error occured while running the udm command. STDERR: %s" % stderr)
        if not stdout.strip():
            utils.fail(
                "The udm command did not produce any output to STDOUT, while a computer with a name "
                "'%s' was expected to be listed" % computer_name
            )

        if computer_name not in stdout:
            utils.fail(
                "The computer with a name '%s' was not listed in the udm command output" % computer_name
            )

    def generate_windows_client_ldif(self, computer_name):
        """
        Fills the sample ldif with a relevant data of a test Windows Client;
        Returns computer ldif.
        """
        computer_ldif = (
            "dn: CN={computer_name},CN=Computers,{ldap_base}\n"
            "objectClass: computer\n"
            "cn: {computer_name}\n"
            "name: {computer_name}\n"
            "userAccountControl: 4096\n"
            "sAMAccountName: {computer_name}$\n"
            "dNSHostName: {computer_name}.{domain_name}\n"
            "servicePrincipalName: HOST/{computer_name}.{domain_name}\n"
            "servicePrincipalName: RestrictedKrbHost/{computer_name}.{domain_name}\n"
            "servicePrincipalName: HOST/{computer_name}\n"
            "servicePrincipalName: RestrictedKrbHost/{computer_name}\n"
            "objectCategory: CN=Computer,CN=Schema,CN=Configuration,{ldap_base}\n"
            "operatingSystem: Windows 7 Professional\n"
            "operatingSystemServicePack: Service Pack 1\n"
            "operatingSystemVersion: 6.1 (7601)\n"
            "userPrincipalName: host/{computer_name}.{domain_name}@{kerberos_realm}"
        ).format(
            computer_name=computer_name,
            ldap_base=self.UCR.get("samba4/ldap/base"),
            domain_name=self.domain_name,
            kerberos_realm=self.UCR.get("kerberos/realm"),
        )

        return computer_ldif

    def create_computer_via_ldbadd(self, computer_name):
        """
        Using 'ldbadd' creates a computer account (Windows 7 client) with a
        given computer_name and generated ldif.
        """
        computer_ldif = self.generate_windows_client_ldif(computer_name)
        print(
            "\nCreating a Windows Client Computer for the test with a name  %s via 'ldbadd':"
            % computer_name
        )

        cmd = (
            "ldbadd",
            "-H",
            "ldap://" + self.UCR.get("ldap/server/name"),
            "-U",
            self.admin_username + "%" + self.admin_password,
        )

        stdout, stderr = self.create_and_run_process(cmd, PIPE, computer_ldif)
        # Workaround for Bug #53711
        if workaround.filter_deprecated(stderr):
            utils.fail(
                "An error occured while creating computer in SamDB using 'ldbadd' tool: '%s'" % stderr
            )
        print(stdout)
        if not stdout.strip():
            utils.fail(
                "The 'ldbadd' tool did not produce any output to STDOUT, while a confirmation of "
                "computer record add was expected"
            )
        else:
            print("The 'ldbadd' produced the following output:", stdout)
        if not bool(search(".*Added 1 record.* successfully", stdout, IGNORECASE)):
            utils.fail(
                "Could not find the confirmation of a successful record addition to the database from "
                "'ldbadd'."
            )

    def remove_computer_via_udm(self, computer_name):
        """
        Removes the computer with the given 'computer_name' using
        'UCSTestSchool' functionality.
        """
        print("\nRemoving the computer with a name: '%s':" % computer_name)

        school_ou_dn = self.UCR.get("ldap/hostdn")
        school_ou_dn = school_ou_dn[school_ou_dn.find("ou=") :]

        # the computer DN should be full including its OU:
        computer_dn = "cn=%s,cn=computers,%s" % (computer_name, school_ou_dn)

        TestSchool = UCSTestSchool()
        TestSchool._remove_udm_object("computers/windows", computer_dn)

    def main(self, precreate_computer=False):
        """
        Creates a Windows Client in the SamDB and checks:
         - Client join Replica Directory Node with a pre-created computer account when
           'precreate_computer' is True;
         - Client join Replica Directory Node without a beforehand pre-created
           computer account.
        """
        self.get_ucr_test_credentials()

        private_dir = ""  # temporary Samba private directory
        computer_name = "WIN7" + random_username(6).upper()
        self.domain_name = self.UCR.get("domainname")
        try:
            private_dir = self.configure_samba_credentials_lp()

            if precreate_computer:
                self.create_computer_via_ldbadd(computer_name)
                sleep(30)  # wait for replication
                utils.wait_for_replication()

            machine_password = self.join_a_computer(computer_name)
            self.check_computer_via_udm(computer_name)
            self.check_sysvol_access(computer_name, machine_password)
        finally:
            if computer_name:
                self.remove_computer_via_udm(computer_name)
            if private_dir:
                print("\nRemoving temporary Samba private dir:", private_dir)
                rmtree(private_dir, True)


if __name__ == "__main__":
    # Test cases are run as a two separate processes to avoid
    # "Bad talloc magic value - unknown value" error raised when
    # joining more than one computer in a single process.
    # Both processes are run with a timeout.
    # Test would fail if any process has exit code different from 0 or None.
    TestWindowsClientJoin = TestS4ClientJoinIntoSchool()

    # case 1: first create a computer in ldb and than join it:
    print("Running test case 1: join of a Windows client with a pre-creation of a computer account:")
    TestProcess = Process(target=TestWindowsClientJoin.main, args=(True,))
    TestProcess.start()
    TestProcess.join(150)

    if TestProcess.exitcode:
        utils.fail("There were errors during join with a pre-created computer account, test failed.")

    ### Case 2 currently fails: Bug #37698
    ### # case 2: join without a pre-created computer account:
    ### print("\nRunning test case 2: join of a Windows client with no computer account pre-created
    ### beforehand:")
    ### TestProcess = Process(target=TestWindowsClientJoin.main)
    ### TestProcess.start()
    ### TestProcess.join(120)
    ###
    ### if TestProcess.exitcode:
    ### 	utils.fail("There were errors during join without a pre-created computer account, test failed.")  # noqa: E501
