#!/usr/share/ucs-test/runner python
## desc: Test Window client join into UCS@School S4-Slave.
## bugs: [34219]
## roles:
##  - domaincontroller_slave
## packages: [univention-samba4]
## tags: [apptest, ucsschool]
## exposure: dangerous

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

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

from essential.test_samba4 import TestSamba4

import univention.testing.utils as utils
from univention.testing.ucsschool import UCSTestSchool
from univention.testing.strings import random_username


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")

        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)
        if stderr:
            utils.fail("An error occured while creating computer in SamDB "
                       "using 'ldbadd' tool: '%s'" % stderr)
        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(match('.*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 DC-Slave with a pre-created computer account when
           'precreate_computer' is True;
         - Client join DC-Slave without a beforehand pre-created
           computer account.
        """
        self.get_ucr_test_credentials()

        private_dir = ''  # temporary Samba private directory
        computer_name = 'WIN7PRO4_' + 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

            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: 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.")
