Pull to refresh

Автоматизация работы со статическими маршрутами на сети FreeBSD-серверов

Reading time5 min
Views3.4K

С чего всё началось


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

Реализация


Исходим из того, что у нас есть некоторое количество серверов под управлением FreeBSD, и при добавлении нового маршрута (например, на одном из этих серверов была затерминирована новая клиентская сеть) он должен быть прописан на всех этих серверах.
Для начала создадим текстовый файл, каждая строчка которого являет собой адрес одного сервера, и для каждого из этих серверов настроим авторизацию по ключу (как это сделать, описано, например, здесь).
Пример файла-списка серверов:
192.168.0.1
192.168.1.1
mainserver.yourdomain.ru


Дальнейшую работу будет делать связка из двух shell-скриптов:
1) checkroute.sh [-c] destination gateway, где
-c — опция check-only (только проверить соответствие, не внося изменений),
destination — IP адрес или сеть назначения,
gateway — шлюз для адреса назначения.
Этот скрипт проверяет валидность введённых параметров скрипта и, подключаясь по очереди к каждому из серверов из списка по ssh, копирует на него второй скрипт и запускает его.
2) checker.sh destination gateway ifcheck
В свою очередь, проверяет текущую таблицу маршрутизации и конфигурационные файлы, докладывает о результатах проверки и, если первый скрипт был запущен без опции -c, приводит соответствующие записи в соответствие с парой destination/gateway.

checkroute.sh

Первым делом проверим опции, с которыми был запущен скрипт. На данный момент, обрабатывается только опция -c, но этот функционал легко расширяется по аналогии.

ifcheck="no"
while getopts "c" optname
	do case "$optname" in
		"c") ifcheck="check" ;;
		*)   echo "Usage: $0 [OPTIONS] DESTINATION GATEWAY
\t-c\tcheck route, don't correct it
"
			exit
			;;
		esac
	done
shift $(($OPTIND -1 ))

Следующим шагом будет проверка на валидность параметров DESTINATION и GATEWAY, переданных скрипту. Для наших целей необходимо, чтобы в качестве DESTINATION могли выступать IP адрес или сеть, а в качестве GATEWAY — только IP адрес.

validIPregexp="(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
checkip () {
	CHECK=$(echo $1 | grep -E "^$validIPregexp$2")
	if [ ! "$?" -eq 0 ]
		then
		echo "Incorrect $3: $1, please try again. "
		exit
		fi
}
checkip $1 "(/([0-2]?[0-9]|3[0-2]))?$" "destination IP address or network"
checkip $2 "$" "gateway"

Далее построчно читаем файл $serverlist и для каждого сервера из списка инициализируем подключение по ssh, копируем и запускаем скрипт checker.sh. В переменной $dir хранится путь до директории, в которой лежат файлы $serverlist и checker.sh

serverlist='servers.lst'
dir='/usr/local/etc/your_dir'
cat "${dir}/$serverlist" | while read server
	do
	echo "$server: "
	cat "${dir}/$checker.sh | ssh -q $server "rm -f /var/tmp/checker.sh ; cat - >> /var/tmp/checker.sh ; sh /var/tmp/checker.sh $1 $2 $ifcheck "
	done

checker.sh

Теперь рассмотрим, что же делает скрипт checker.sh, будучи запущенным на одном из наших серверов.
Функция, применяющая изменения, если скрипт script.sh был запущен без опции -c

ifcheck=$3
commit () {
	if [ $ifcheck != "check" ]
		then
		eval "$1" 2> /dev/null
		if [ ! $? -eq 0 ]
			then
			echo "Couldn't process the following command:
$1"
			else echo "$2"
			fi
		else echo "To correct it run this script without \"-c\" option or process the following command manually:
$1
"
		fi
}


Разбираем текущую таблицу маршрутизации (вывод netstat -rnW) и результат записываем в $CurrentGW
Возможные варианты:
1) Адрес назначения затерминирован на локальном интерфейсе ($CurrentGW=«LOCAL»)
2) Адрес назначения затерминирован где-то ещё и присутствует в таблице маршрутизации, тогда $CurrentGW будет присвоен текущий шлюз для этого адреса
3) Адрес назначения затерминирован где-то ещё и отсутствует в таблице маршрутизации. В таком случае, $CurrentGW останется пустой.

CurrentGW=`netstat -rnW | awk -v gw="$2" -v src="$1" '{ if ( $1 ~ src ) { if ( $2 ~ /'$validIPregexp'/ ) { if ( $2 !~ gw ) { print $2 } else { print "OK" } } else { print "LOCAL" } } }'`
if [ -z $CurrentGW ]
	then
	echo "Route for $1 does't exist."
	commit "sudo route add $1 $2" "Adding route."
	else
	case $CurrentGW in
	OK )  echo "Current route to $1 is $2 : OK" ;;
	LOCAL ) echo "It's on local interface" ;;
	* ) echo "Route for $1 exists but destination is wrong: $CurrentGW instead of $2 " ; commit "sudo route change $1 $2" "Changing it." ;;
	esac
	fi

Далее разбираем конфигурационный файл, полный путь к которому лежит в переменной $filename. Для этого выбираем из него все строки, описывающие добавление маршрутов, во временный файл и проверяем существование записи для адреса назначения. Проводится аналогично разбору вывода netstat -rnW выше.

filename='/usr/local/etc/rc.d/rc.script.for.adding.routes.sh'
cat "$filename" | grep -v '^#' | sed '/^$/d' | sed 's/[ \t]+/ /g' | grep -E 'route.+add' > /var/tmp/routes_temp.lst
FileGW=`cat /var/tmp/routes_temp.lst | grep $1 | awk -v gw="$2" '{  if ( $NF ~ gw ) { print "YES" } else { print $NF } }'`
if [ $CurrentGW = "LOCAL" ]
	then if [ ! -z $FileGW ]
		then
		echo "It's on local interface, but route for $1 was found in $filename. "
		commit "sudo sed -Ei '' '\:route[^\n]+add[^\n]+$1:' $filename" "Deleting it from there."
		fi
	else
	if [ -z $FileGW ]
		then
		echo "Checked $filename: no match. "
		t1=`tail -1 /var/tmp/routes_temp.lst | awk '{ print $(NF-1) }'`
		t2=`tail -1 /var/tmp/routes_temp.lst | awk '{ print $NF }'`
		IFS=''
		regex="\\:route[^\\n]+add[^\\n]+$t1[ \\t]+$t2:a\\
		/sbin/route -nq add $1 $2

		"
		commit "sudo sed -Ei '' '$regex2' $filename" "Adding route to the end of file"
		else
		if [ $FileGW != "YES" ]
			then
			echo "Record for route to $1 found, but gateway is wrong: $FileGW. "
			regex="s|$1[ \t]+$FileGW|$1 $2|g"
			commit "sudo sed -Ei '' '$regex2' $filename" "Changing gateway to $2"
			else echo "Correct route was found in $filename: OK"
			fi
		fi
fi

Хочу прояснить пару моментов из последнего куска кода. Переопределение системной переменной IFS понадобилоть для того, чтобы создать многострочную переменную $regex, что, в свою очередь, продиктовано синтаксисом sed. Такой способ добавления строки в файл был выбран потому, что в $filename маршруты могут добавляться внутри одной из функций, как это происходит в моём случае. Если же для вас это не критично, вы можете обойтись конструкцией вида

commit "sudo echo '/sbin/route -nq add $1 $2' >> $filename"

Заключение


Описанные выше скрипты можно адаптировать и под другие *nix системы, вся разница — в ключах sed (для Debian, например, -ri вместо -Ei "") и синтаксисе команды route (см. man route). Функционал скриптов, как было уже сказано выше, легко расширяется по аналогии.
В скриптах довольно много проверок («защита от дурака»), однако ответственность за правильность пары destination/gateway ложится на администратора, запускающего скрипт. Будьте бдительны!
Полные тексты обоих скриптов прилагаются: checkroute.sh, checker.sh
Tags:
Hubs:
Total votes 33: ↑24 and ↓9+15
Comments28

Articles