Автоматическая сортировка пользователей по группам в webmin

Одной из полезных возможностей Webmin является вызов пользовательского скрипта до и после действий по созданию, изменению, удалению пользователей и групп (pre и post hook). В нашей работе по школе мы задействовали этот механизм для автоматического создания папок со ссылками на домашние директории пользователей. Каждая папка представляла собой учеников одного класса.

Для чего все это создавалось

Домашние директории удобно хранить в одном каталоге, расположение которого не меняется. Такая система отлично подходит для администрирования, но совершенно неудобна для случая, когда нужно работать с файлами небольшого подмножества пользователей, объединенных в одну группу. Для решения этой проблемы, можно воспользоваться замечательном средством Unix систем — символическими ссылками.

Использование ссылок позволяет хранить домашние директории в одном месте и параллельно иметь директории, в которых сгруппированы не сами домашние директории пользователей, а ссылки на них. Ссылки должны быть относительными для корректной работы по NFS и возможности монтировать шару с пользователями куда угодно на удаленном компьютере.

Для решения наших задач был выбран следующий шаблон относительного пути:

 
../../$USERS_HOME_BASE_DIR/$USERNAME

При такой относительной структуре ссылок мы можем как угодно группировать пользователей при следующих двух ограничениях:

  1. Возможна только плоская группировка, т.е. все возможные группы располагаются на одном уровне
  2. Ссылки жестко привязаны только к названию домашнего каталога с пользователями. Папка с пользователями должна монтироваться под тем же именем, которое она имеет на сервере. Это ограничение можно обойти созданием дополнительной символической ссылки на компьютере который монтирует сетевую NFS шару, но будем считать такой вариант слишком хлопотным
Никто не мешает поддерживать заданную структуру руками, но спихнем эту задачу на машину.

Пишем post-hook

Немного о механизме пользовательских скриптов

Webmin передает параметры в скрипт путем установки целого ряда переменных среды, которые имеют префикс USERADMIN. На вики страничке проекта имеется описание некоторых переменных. К сожалению описание неполное, существуют и другие переменные. Самым простым вариантом их просмотра является установка в качестве пользовательского скрипта, следующей строчки

env > /tmp/webmin-env
После этого достаточно выполнить интересующее действие, например создать или изменить группу, и забрать файл /tmp/webmin-env в котором найти все установленные переменные с префиксом USERADMIN.

Остановлюсь подробно только на одной важной переменной USERADMIN_ACTION. В ней содержится код действия, в процессе выполнения которого был запущен пользовательский скрипт. Если тип действия влияет на работу скрипта, то сначала необходимо проверить значение этой переменной. С возможными значениями можно ознакомится пройдя по ссылке выше.

Разбираемся с папками для групп

От папок нужно только своевременное создание, во время создания новой группы и удаление вместе со всеми ссылками во время удаления группы. Для большей гибкости этого процесса добавим следующие требования

  1. Директории создаеются только для групп, имя которых соответсвует заданному шаблону
  2. На созданной папке меняется группа и задаются специальные права доступа
Посмотрим на скрипт который делает все необходимое.

#!/bin/bash

#шаблон для названий групп
GROUP_PATTERN="^class[0-9]+[:alpa:]*"

#Директория в которой будут создаваться каталоги для групп
CLASS_GROUPS_DIR=/srv/user_dirs/groups_dir

#группа и права
DIR_GROUP=class_admins
DIR_RIGHTS=775

group_name=$USERADMIN_GROUP

function create_group_dir(){
    if echo $group_name | grep -qE $GROUP_PATTERN - ;
    then
	new_dir_path="$CLASS_GROUPS_DIR/$group_name"
	mkdir "$new_dir_path"
	chgrp $DIR_GROUP "$new_dir_path"
	chmod $DIR_RIGHTS "$new_dir_path"
    fi
}

function remove_group_dir(){
    rm -f "$CLASS_GROUPS_DIR/$group_name/"*
    rmdir "$CLASS_GROUPS_DIR/$group_name"
}

if [[ "$USERADMIN_ACTION" == "CREATE_GROUP" ]];
then
    create_group_dir
elif [[ "$USERADMIN_ACTION" == "DELETE_GROUP" ]]
then
    remove_group_dir
fi

Управляем ссылками

Итак группы созданы и директории для ссылок готовы к заполнению. При разработке скрипта будем считать, что нас интересуют только вторичные группы пользователя, и что пользователь может быть включен сразу в несколько интересующих нас групп.

#!/bin/bash

#Шаблон названия группы
GROUP_PATTERN="^class[0-9]+[:alpa:]*"
#Расположение каталогов групп
CLASS_GROUPS_DIR=/srv/user_dirs/groups_dir
#Путь до домашних директорий пользователей
USERS_DIR=/srv/user_dirs/user_homes

SCRIPT_DIR="$( cd "$( dirname  "$0" )" && pwd )"
source ${SCRIPT_DIR}/common.sh

function get_class_group(){
  for group_id in `echo $1 | tr "," "\n"`;
  do
      group_name=`get_group_name $group_id`
      if echo $group_name | grep -qE ${GROUP_PATTERN} - ;
      then
	  echo $group_name;
      fi
  done
}

function get_group_name() {
    echo `getent group | grep :$1: | head -n 1 | cut -f 1 -d ":"`
}

function make_link_name(){
    echo "$2 ($1)"
}

function create_user_home_link() {
    class_group=$1

    cd "$CLASS_GROUPS_DIR/$class_group/"

    if [[ $? != 0 ]];
    then
	echo "failed to create link in a directory $CLASS_GROUPS_DIR/$class_group"
	return;
    fi

    relative_users_homes=`relpath "$(pwd)" "$USERS_DIR"`
    relative_home_path=$relative_users_homes/`basename $user_home`
    
    link_name=`make_link_name "$user_name" "$user_real_name"`

    ln -s "$relative_home_path" "$link_name"
}

user_name=${USERADMIN_USER}
user_real_name=${USERADMIN_REAL}
secondary_groups=${USERADMIN_SECONDARY}
user_home=${USERADMIN_HOME}


function create_links_for_user(){
    for found_value in `get_class_group $secondary_groups`;
    do
	create_user_home_link "$found_value"
    done
}

function remove_links_for_user(){
    for found_value in `get_class_group $secondary_groups`;
    do
        link_name=`make_link_name "$user_name" "$user_real_name"`
	rm "$CLASS_GROUPS_DIR/$found_value/$link_name"
    done
}

if [[ "$USERADMIN_ACTION" == "CREATE_USER" ]];
then
    create_links_for_user
elif [[ "$USERADMIN_ACTION" == "DELETE_USER" ]];
then
    remove_links_for_user;
fi

Ключевым моментом созданного скрипта является создание относительной ссылки от места её расположения ($CLASS_GROUPS_DIR/$class_group/) до места расположения домашней директории пользователя ($USERS_DIR/<name of user home dir>). За это отвечает не приведенная здесь функция relpath код который был найден на stackoverflow.com.

Обвязка

Webmin предоставляет возможность задать по одной команде для вызова перед и после выполнением административных действий, причем одна команда используется для обработки как пользователей так и групп. Писать один мега-скрипт — дело неблагодарное, указать список последовательно выполняющихся скриптов на форме конфигурации модуля тоже не дело: при добавлении нового скрипта придется не забыть внести правки.

Лучше пойти ленивым путем автоматизации: написать скрипт, который будет в нужном порядке вызывать другие скрипты. Подход стандартный, его не раз можно встретить в системных скриптах:

#!/bin/bash

SCRIPT_DIR="$( cd "$( dirname  "$0" )" && pwd )"

SH_DIR="$SCRIPT_DIR/run.d"

if [[ -d $SH_DIR ]]; 
then
	cd $SH_DIR
	for shell_file in $SH_DIR/[0-9]*.sh ;
	do
		$( $shell_file )
	done
fi

Результаты, дальнейшее развитие

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

Весь код, представленный в посте можно взять в нормальном виде на github.com и использовать на свое усмотрение в соответcтвии с лицензией MIT

UPD 12.10.2011: код приведен в актуальное состояние с репозиторием

1 комментарий: