1. Предпосылки


Настраивая сервер zimbra столкнулся с проблемой синхронизации пользователей по группе в Active Directory (AD). Если мы создаем нового пользователя в AD, то он нормально добавлялся, но если мы делаем доступ к почтовому серверу по группе, то первый раз все пользователи синхронизируются. А вот потом, изменения в группе никак не влияют на изменения пользователей почтового сервера zimbra.

Что не понравилось в этих статьях, это использование скрипта на powershell (зачем, если есть ldapsearch) и постоянный вызов утилиты zmprov, и когда идет синхронизация большого количества пользователей, то скрипт выполняется продолжительное время

2. Исходные данные


ОС сервера: CentOS 7
Язык скрипта: bash
Домен Zimbra: test.ru
Сервер Zimbra: zimbra.test.local
Домен Active Directory: test.local
Группа AD для доступа к почте: mail

У пользователя может быть почта отличающаяся от его логина, такую почту вносим в AD в поле mail и по нему создаем алиас в zimbra (например вася пупкин входит в систему под логином vasia, но почту должен отправлять и получать еще как пользователь v.pupkin@test.ru)

3. Схема работы скрипта


  1. Сохраняем в файл пользователей AD входящих в группу mail
  2. Сохраняем в файл всех пользователей zimbra со всеми атрибутами
  3. Разделяем файл со списком всех пользователей zimbra на файлы формата: имя файла — логин пользователя, содержание — атрибуты пользователя
  4. Убираем из списка пользователей zimbra системные учетные записи (admin, gal,antivirus)
  5. Сравниваем список пользователей AD и zimbra
  6. Создаем файл с командами добавления, удаления (в моем случае блокировки пользователя), синхронизации и создания алиасов
  7. Применяем данные действия в zimbra (один вызов zmprov)
  8. Отправляем отчет на почту администратору (если есть что отправлять)
  9. Удаляем временные файлы и каталоги

4. Скрипт синхронизации


У скрипта есть два режима работы — это запуск без параметров, тогда отработают только блокировка и добавление пользователей. И запуск с параметром «all», тогда будут синхронизированны все пользователи группы mail в AD.

Так же необходимо обратить внимание на использование утилиты декодирования base64 в функции синхронизации, ее необходимо использовать для полей AD в которых используются русские символы.

Сам скрипт:
#!/bin/bash
#
#1. Определение переменных
#
#1.1 Общие переменные
#Путь к рабочему каталогу
path="/mnt/zimbra/user-sync"
#Временная метка
timestamp=`date +%F-%H-%M`
#путь к временным файлам
tmp_dir=$path/tmp
#Путь к файлам с атрибутами пользователей zimbra
zim_us=$tmp_dir/zim-us
#путь к файлам логов
log_dir=$path/log
#имя лог-файла
log=$log_dir/grouplog_$timestamp.txt
#путь ко временному файлу со списком пользователей
usname=$tmp_dir/usname
#Путь к файлу со списком команд на пакетное выполнение утилитой zmprov
zmcmdfile=$tmp_dir/zmcmdfile
#путь ко временным файлам со списком атрибутов пользователей AD 
userfil=$tmp_dir/userfil
#отправка почты
mutt="/usr/bin/mutt"

#
#1.2 переменные сервера zimbra
#имя домена Zimbra
domain="test.ru"
#путь к командлету zmprov
zmprov="/opt/zimbra/bin/zmprov"
#
#1.3 переменные доступа к AD по LDAP
#LDAP search
ldapsearch=/opt/zimbra/common/bin/ldapsearch
# Подключение к серверу (либо сервер, либо домен (если больше одного сервера))
ldap_server="ldap://test.local:389"
#Базовая OU поиска
basedn="DC=test,DC=local"
#Пользователь и пароль для доступа к AD по LDAP
binddn="CN=zimbra,CN=Users,DC=test,DC=local"
bindpw="qwe123" #user password 
#Фильтр поиска - кто входит в группу mail
filter="(memberof=cn=mail,cn=users,dc=test,dc=local)"
#какие поля данных ищем (логин, почту и поле с не совпадающей с логином почтой для алиаса и подписи)
fields="sAMAccountName mail description displayName givenName cn sn department title"
#конец блока переменных

#Начинаем обработку

#Функции обработки
#Запись ошибки в лог
function err_log()
{
if [ $1 -eq 0 ]; then
		#echo -n "$(tput hpa $(tput cols))$(tput cub 6)[OK]"
		#echo
		echo $2" [Ok]"  >> $log
	else
		#echo -n "$(tput hpa $(tput cols))$(tput cub 6) [FAIL]"
		#echo
		echo  $2" [Fail]" >> $log
	fi
}

#Проверка существования каталога
function if_path ()
{
	#Если каталог не существует то создаем его
	if [ ! -d $1 ]; then
	#Создание каталога для обработки
	echo "Создание каталога $1..." >> $log
	mkdir -p $1
	err_log $? "Проверка каталога $1"
	
else
	echo "Каталог обработки $1 существует" >> $log	
fi
}

#Запись в файл списка пользователей из AD
function searchusersAD()
{
	echo "Запись списка пользователей рассылки из AD..." >> $log
	$ldapsearch -x -o ldif-wrap=no -H $ldap_server -D $binddn -w $bindpw -b $basedn $filter $fields | 
	grep sAMAccountName | 
	egrep -v '^#|^$' | 
	awk '{print $2}' |
	sort > $usname.ad
	echo "Found (Найдено) "`cat $usname.ad | wc -l`" Group in AD (групп в AD)" >> $log
}

function alluserattrzimbra()
{
	#Создаем файл со списком всех пользователей и их атрибутов
	$zmprov -l gaa -v $domain > $usname.gaa
	#Переходим в папку где будут созданы файлы с атрибутами пользователей
	cd $zim_us
	#разбиваем файл со всеми пользователями на файлы, с атрибутами только одного пользователя
	csplit $usname.gaa --prefix='user.' --suffix-format='%03d.zim' --elide-empty-files -s /"# name"/ '{*}'

	#Переименовываем файлы по пользователям. имя берем из имени пользователя zimbra в файле
	for i in $( ls $zim_us )
	do
		nam=`grep "# name" $zim_us/$i | awk '{ print $3}' | sed 's/@.*//g'`
		mv -f $zim_us/$i $zim_us/$nam
	done
	cd $path
}

#Запись в файл списка пользователей из zimbra 
function searchuserzimbra()
{
	echo "Запись списка пользователей из zimbra..." >> $log
	ls $zim_us | sort > $usname.tem
	#Удаляем системные аккаунты из проверки. файл $path/system.acc содержит список системных пользователей
	diff -u -i $usname.tem $path/system.acc  | sed 1,3d | grep ^- | cut -c 2- | sort > $usname.zim
	#rm -f $usname.tem
	echo "Found (Найдено) "`cat $usname.zim | wc -l`" Group in Zimbra (групп в Zimbra)" >> $log
	
}

#Разница между списками пользователей (для добавления или блокировки
function diffuserlist()
{
	diff -u -i $usname.zim $usname.ad | sed 1,3d | sed '/@.*/d' > $usname.diff
}

#добавление пользователей
function adduser()
{
#Проверяем существование пользователя на добавление	
adddif=`grep ^+ $usname.diff | sed '1!d'`
	if [ -n $adddif ];
	then
		for addus in $( grep ^+ $usname.diff | cut -c 2- )
		do
			# проверяем есть ли такой пользователь в zimbra (если есть - разблокируем, нет - создаем)
                        ifclos=`grep "zimbraAccountStatus:" $zim_us/$addus | awk '{print $2}' | cut -c -1`
			if [ $ifclos = "c" ];
			then
				echo "ma $addus@$domain zimbraAccountStatus active" >> $zmcmdfile
				echo "Пользователь $addus разблокирован" >> $tmp_dir/send.txt
				if [ $addus != "" ];
				then
					sync_one_user $addus
				fi
			else
				#123456 - пароль пользователя, необходим для создания пользователя. может быть любым, так как  настроена проверка паролей в домене AD
				echo "ca $addus@$domain 123456" >> $zmcmdfile
				echo "Пользователь $addus создан" >> $tmp_dir/send.txt
				if [ $addus != "" ];
				then
					sync_one_user $addus
				fi
			fi
		done
		
	fi
}

#блокировка пользователей перед отключением
function blockuser()
{
	deldif==`grep ^- $usname.diff | sed '1!d'`
	if [ -n $deldif ];
	then
		for delus in $( grep ^- $usname.diff | cut -c 2- )
		do
			#zimbraAccountStatus closed
			if [ $delus != "" ];
			then
				ifclos=`grep "zimbraAccountStatus:" $zim_us/$delus | awk '{print $2}'`
				if [ "$ifclos" != "closed" ];
				then
					echo "user closed - $delus"
					echo "ma $delus@$domain zimbraAccountStatus closed" >> $zmcmdfile
					echo "Пользователь $delus заблокирован! Удалите его с сервера самостоятельно!" >> $tmp_dir/send.txt
					echo $delus >> $path/close.1
					cat $path/close.1 | sort > $path/close.diff
					echo "$delus" 
				fi
			fi
		done
	fi
}

#Функция проверки существования атрибута
function ifattr()
{
	if1char=`echo $2 | cut -c -1`
	if [[ -n $2 && $if1char != "" ]];
	#if [ $2 != "" ];
	then 
	    #echo $2
	    echo -n " $1 \"$2\""  >> $zmcmdfile
	fi
}

#функция синхронизации пользователя
function sync_one_user()
{
	echo "Синхронизация пользователя $1..." >> $log
	$ldapsearch -x -o ldif-wrap=no -H $ldap_server -D $binddn -w $bindpw -b $basedn "(sAMAccountName=$1)" $fields > $userfil/$1.ad
	
	#Создаемначало строки атрибутов синхронизации
	echo -n  "ma "$1 >> $zmcmdfile
	
	#samacc=`grep "sAMAccountName:" $userfil/$1 | awk '{print $2}'`  
		
	description=`grep "description:" $userfil/$1.ad | awk '{split ($0, a, ": "); print a[2]}'`
	#echo $description
	ifattr "description" "$description"
		
	displayName=`grep "displayName:" $userfil/$1.ad | awk '{split ($0, a, ": "); print a[2]}' | base64 -d`
	ifattr "displayName" "$displayName"
	
	givenName=`grep "givenName:" $userfil/$1.ad | awk '{split ($0, a, ": "); print a[2]}' | base64 -d`
	ifattr "givenName" "$givenName"
	
	cn=`grep "cn:" $userfil/$1.ad | awk '{split ($0, a, ": "); print a[2]}'`
	ifattr "cn" "$cn"
	
	sn=`grep "sn:" $userfil/$1.ad | awk '{split ($0, a, ": "); print a[2]}' | base64 -d`
	ifattr "sn" "$sn"
	
	department=`grep "department:" $userfil/$1.ad | awk '{split ($0, a, ": "); print a[2]}' | base64 -d`
	ifattr "company" "$department"
	
	title=`grep "title:" $userfil/$1.ad | awk '{split ($0, a, ": "); print a[2]}' | base64 -d`
	ifattr "title" "$title"
	
	#Вставляем перевод строки в файле
	echo >> $zmcmdfile
	#Добавляем алиас
	mailnew=`grep "mail:" $userfil/$1.ad | awk '{print $2}'`
	if [ "$mailnew" != "" ];
	then 
	# [ -n $mailnew ] 
	#    echo $2
		#Проверяем наличие алиаса у пользователя
		#${1,,} - приводим все символы к нижнему регистру
		useralias=`grep "zimbraMailAlias:" $zim_us/${1,,} | awk '{print $2}'`
	    if [ $useralias != $mailnew ];
		then
			echo "aaa \"$1@$domain\" \"$mailnew\""  >> $zmcmdfile
		fi
	fi
	#ifattr "mail" $mailnew
#	echo $mailnew
	echo "Синхронизация пользователя $1 " >> $tmp_dir/send.txt 
	
#	echo "Пользователь $1 - $atrruser"
	
	#echo "Found (Найдено) "`cat $usname.ad | wc -l`" Group in AD (групп в AD)" >> $log
}


#Выполнение скрипта
date +%F-%H-%M
#2.Проверка существования каталогов
#Корневой каталог обработки
if_path $path
#Каталог временных файлов
if_path $tmp_dir
#Каталог лог-файла
if_path $log_dir
#Каталог со списком групп
if_path $userfil
#каталог со файлами пользователей зимбра
if_path $zim_us

#Очищаем файл со списком команд на пакетное выполнение утилитой zmprov
:> $zmcmdfile
# Очищаем тело письма администратору
:> $tmp_dir/send.txt
#3.Создаем список групп рассылки из AD 
searchusersAD

#4.Создаем список существующих в zimbra пользователей
alluserattrzimbra
#удаляем лишних (системных)
searchuserzimbra

#5.Сравниваем оба списка групп рассылки
diffuserlist
#Блокируем пользователей
blockuser
#Создаем или разблокируем новых пользователей
adduser

#tckb скрипту передан параметр "all" при запуске, то синхронизируем всех пользователей находящихся в группе mail AD
if [[ -n $1 && $1 = "all" ]];
then
	for us in $(cat $usname.ad );
	do
	#	echo $us
		sync_one_user $us
	done
fi
# запускаем выполнение всех команд утилитой zmprov из файла
$zmprov -f $zmcmdfile
# дописываем в лог файл с командами 
cat $zmcmdfile >> $log
#Отправляем письмо с изменениями админу (если они есть)
if [ -s $tmp_dir/send.txt ];
then
	$mutt  -s "Синхронизация списка пользователей $timestamp" admins@test.ru -a $log < $tmp_dir/send.txt
fi

#Удаляем временные файлы и каталоги
rm -R -f $tmp_dir



5. Заключение


В целом скрипт получился довольно шустрый, утилита zmprov используется всего два раза, остальные утилиты и функции отрабатывают намного быстрее.

6. Ссылки


При создании данной статьи использовались идеи и статьи:

1. Сысоева Андрея
2. DruGoeDeLo
3. Мой коментарий к статье 1) со скриптом для синхронизации списков рассылки