#!/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
## versions:
##  4.0-0: skip
##  3.2-5: skip
##  4.0-1: skip
##  4.1-2: fixed

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")
		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)
		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 = '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.")
