Pull to refresh

Автоматическая настройка FreeBSD

Reading time 9 min
Views 8.8K

Добрый час, Хабровчане!



Хочу поделиться своим опытом автоматизации процесса установки и настройки FreeBSD с помощью sh (bash). Дело было так:
Однажды в компании возникла необходимость поднять несколько серверов на FreeBSD. Поставив одну, следом за ней вторую и третью ось, мы с коллегой (в штате всего два айтишника) задумались в сторону автоматизации этого процесса путем написания скрипта, выполняющего настройку свежеустановленной ОС. Задача написания легла на мои плечи. Коллега занялся решением вопроса автоматической установки, о чем я расскажу в другом посте. Итак, приступим!

Сразу уточню: скрипт писался исключительно для себя, поэтому некоторый код не оформлен должным образом или просто «не идеален». Но, тем не менее, он работает!
Скрипт состоит из 3 файлов: файл-загрузчик, основной исполняемый файл и файл с библиотеками. Начну по порядку.

loader.sh

#!/bin/sh

lib1='parser.sh'					#CORE FILE
lib2='parserlib.sh'					#LIB FILE

#Defining variables --begin--

FTPServer='ftp://10.10.10.50'			#FTP server address
FileName='tarball.tar.gz'			#Default tarball name
LongVersion=`uname -r`				#Long name of current OS version
ShortVersion=${LongVersion%%-*}			#Calculatig short version of OS, like "8.3" (DON'T TOUCH THIS LINE, please!)
LocalDirectory='/tmp/script/'			#Temporary directory. You can manualy clean it later. If it's not exist - just relax, script will create it for you :-)
DataDirectory='/var/parser-md5-list/'		#Directory with datafiles
LockFile='EditLocker.p'				#Lock file. Needed to prevent changes in files by script after manual editing.
EditLock="$DataDirectory$LockFile"		#Absolute LockFile path
forcer='-no-force'				#Variable must be not empty in case if -f key was not specified

#Defining variables --end--

usage (){
echo "Only acceptable options:
-f to force rebuild without check
-s to force execution without restart
-d to delete locker file (needed if files was edited manyally or by system, but now you want to rebuild it)
-v <version> to manually specify FreeBSD version. Please be sure to specify it correctly! Example: ./loader.sh -v 9.1
"
}


#Defining options --begin--

if [ $# -ge 1 ]
 then
	while getopts fsdv: opt; do
		case "$opt" in
			f) forcer='-f';;
			s) skip='-s';;
			d) DL='-d';;
			v) ShortVersion="$OPTARG";;
			[?]) usage; exit 1;; 
		esac
	done
fi

#Defining options --end--

echo "FreeBSD configuration tool V 1.0"
echo "Detected OS version: $LongVersion"
echo "Applying tarball for version $ShortVersion"
echo; echo "Downloading files..."
if [ "$DL" == "-d" ]
	then
		touch $EditLock 		#If file does not exists - this line will create it (needed to avoid error message from rm)
		rm $EditLock			#Remove Lock file
fi
if ! [ "$skip" == "-s" ]
 then
	if ! [ -e run.sh ]
	 then
		fetch "$FTPServer/FreeBSD/loader.sh"
		touch run.sh
		echo "#!/bin/sh
clear
echo \"***********************************************
* Loader file was updated. Process restarted. *
***********************************************
\"
" >> run.sh
		echo "./loader.sh $@" >> run.sh
		chmod +x run.sh
	        ./run.sh
		exit 1
	 else
		rm run.sh
	fi
fi
fetch "$FTPServer/FreeBSD/$lib1"
fetch "$FTPServer/FreeBSD/$lib2"
chmod +x $lib1
chmod +x $lib2
. $lib1							#Attaching core file to process
if [ $? -ne 0 ]						#Checking for errors
then echo "ERROR! Library $lib1 not found!"		#Core file does not exist.
exit 1
fi

LetItGo $FTPServer $FileName $ShortVersion $LocalDirectory $forcer $DataDirectory $EditLock


parser.sh

#!/bin/sh

LetItGo()			#Body of script
{
lib1='parserlib.sh'					#Lib file
. $lib1							#Attaching lib file to process
if [ $? -ne 0 ]						#Checking for errors
then echo "ERROR! Library $lib1 not found!"		#Lib file does not exist.
	kill $$
	exit 1
fi

#Defining variables --begin--

server=$1			#FTP server address
file=$2				#Default tarball name
ver=$3				#Version of OS, like "8.3"
LocalDir=$4			#Temporary directory. You can manually clean it later. If it's not exist - just relax, script will create it for you :-)
DataDirectory=$6		#Directory with data files
EditLock=$7			#This file needed to prevent overriding for manually edited files.

#Defining variables --end--

cdOrCreate $DataDirectory				#Creating data directory if not exists
cdOrCreate $LocalDir					#Enter temporary directory for file downloads
dirchek=`pwd`/
if [ "$dirchek" == "$LocalDir" ]			#Checking current directory
 then
	rm -rf *						#If script successfully entered the temp directory - it will be cleaned
 else
	echo "$LocalDir is not accesible! Please check permissions!"
	kill $$
	exit 1
fi

fetch "$server/FreeBSD/$ver/$file"			#Download tarball
hshchk=$(md5 $file)
HashCheck ${hshchk##* } $LocalDir $file $5 $DataDirectory
cd $LocalDir
echo "Extracting files"
tar -zxf $file						#Unpack it
echo "DONE!
" 
rm $file						#Remove tarball
echo "Tarball was removed from local server."
touch $EditLock
echo "Lockfile created: $EditLock"
for f in $( find $LocalDir ); do			#Proceed all files one by one
if [ -f $f ]
 then
								#check file for manual changes
	NEWFILE=${f#$LocalDir}
	NEWFILE=/${NEWFILE#*/}
	HCK=$(md5 $f)
	HCK=${HCK##* }
	#TIMEEDIT="$NEWFILE `stat -f %Sm -t %Y%m%d%H%M%S "$NEWFILE"`"
	EDITC="$NEWFILE $HCK"
	CHECK=`grep "$EDITC" "$EditLock"`
	SIMPLECHECK=`grep "$NEWFILE" "$EditLock"`

#You may add your own subtree in additional elif below (for example: immediately script execution, assigning permissions, e.t.c.)

	if [ "`expr "$f" : '.*\(/Merge/\)'`" == "/Merge/" ]		#If file should be merged
	 then
		TempPath=${f##*/Merge}					#Cut filepath. Original location will remain
		echo; echo "Merge: $f --> $TempPath"
		if ! [ -f $TempPath ]					#If original file exist
		 then
			MoveToLocal $f Merge				#Then just replace it by new one
		 else
			MergeFiles $f $TempPath				#Else - merge new file to the old one line by line
			sort -u $TempPath > $TempPath.tmp			#Delete repeating lines if exists
			mv -f $TempPath.tmp $TempPath			#Rewriting merged file by filtered unique data
			CleanEOL $TempPath					#Cleaning empty lines
		fi
		echo "DONE!"
	 elif [ "`expr "$f" : '.*\(/Replace/\)'`" == "/Replace/" ]		#If file should be replaced
		then
			if [ "$EDITC" == "$CHECK" ] || [ "$SIMPLECHECK" = '' ]
			 then
				echo; echo "Replace: $f --> ${f##*/Replace}"
				MoveToLocal $f Replace					#Then just replace it
				echo "DONE!"
				echo "$EDITC" >> $EditLock
				sort -u $EditLock > $EditLock.tmp       		#Delete repeating lines if exists
				mv -f $EditLock.tmp $EditLock
			 else
				echo; echo "File $NEWFILE was edited manually. Skipped. Use -d key to ignore it."
			fi
	 elif [ "`expr "$f" : '.*\(/Scripts/\)'`" == "/Scripts/" ]		#If tarball contains a scripts, which should have +x permissions
		then
			echo; echo "Replace script: $f --> ${f##*/Scripts}"
			MoveToLocal $f Scripts					#Then replace it (scripts cannot be merged)
			chmod +x ${f##*/Scripts}					#And give eXecution permissions
			echo "DONE!"
	 else
			echo; echo "DON'T match. Cannot proceed $f. Skipping."	#This message means there is another subtree in tarball. It should be removed or described here
	fi
fi
done
echo; echo "===================================================================="
echo; echo "Cleaning temporary files"
cd $LocalDir
dirchek=`pwd`/
if [ "$dirchek" == "$LocalDir" ]     	                   	#Checking current directory
 then
	rm -rf *
	echo "Succesfully cleaned"
 else
	echo "Temporary files was NOT deleted!" 
fi
echo "DONE!"
echo "
Tarball was successfully applied."
echo "To re-apply it again - use force key (-f)."		#Finished
}


И, собственно, функции:
parserlib.sh

#!/bin/sh

cdOrCreate()				#Enter the directory. Create if it's not exist, then enter. Arguments: 1) Path to directory (alternate or absolute).
{ 
	if ! [ -d $1 ]			#If directory does not exists
	 then mkdir -p "$1"	#Then create it
	fi
	cd "$1"				#Enter the directory
}

MoveToLocal()	#Create path and move file there (or replace existing file). Arguments: 1) full filename with full filepath 2) Folder identifyer, without slashes.
{
	TempPath=${1##*/$2}				#Deleting folder identifier from path
	AbsolutePath=${TempPath%/*}			#Completing absolute path
	cdOrCreate $AbsolutePath			#See cdOrCreate() description
	cd ${1%/*}"/"				#Entering directory with file for move
	mv ${1##*/} $AbsolutePath"/"${1##*/}	#Move file to new (absolute path) location
}


MergeFiles()		#Using for check each file from "Merge" subtree and replace lines, or add line to end of file if not exist (?). Files MUST BE in conf syntax. 
{
	cat $1 | while read line
	do
	 lineName=${line%=*}					#Calculating key name
	 lineName="$lineName="	
	 lineHashedName=${lineName##\#}				#Calculating name if commented
	 sed -i -e 's/^'$lineHashedName'.*/'$line'/g' $2		#Replace line with key (uncommented)
	 sed -i -e 's/^#'$lineHashedName'.*/'$line'/g' $2		#Replace line with key (commented by one hash)
	 echo "$line" >> $2						#Append key to the end of file (dublicates will be sorted).
	done
}

CleanEOL()		#This function needed for delete ^M from end of replaced lines and delete every empty line. Arguments: 1) Filename with path.
{
	mv $1 tempconfig.conf
	cat tempconfig.conf | tr -d '\r' > tempconfig.conf.1	#Deleting ^M
	grep '.' tempconfig.conf.1 > $1				#Deleting empty lines and move file to original location
	rm tempconfig.conf*						#Deleting temporary files
}

HashCheck()	#Checks MD5 of tarball. Arguments: 1) Filename 2) Path 3) Tarball name 4) Flag (force rebuild existing installation) 5) Data directory
{
	cdOrCreate "/var/parser-md5-list/"	#See cdOrCreate description
	fpath=$5						#Location of currently downloaded tarball
	pointcheck=$4					#Force flag. If equal to "-f" then check will be skipped
	if ! [ -f $fpath ]					#If checkfile does not exists
	 then
		touch $fpath					#Then create it
		echo $(date) >> $fpath				#And write date and time into it
	 elif ! [ "$pointcheck" == '-f' ]			#If file exists and force flag was not specified
	  then
		cat $1 | while read line			#Then read date and time from existing file
		do						#Show message
		 echo "
==========================================================="
		 echo " This tarball was applied at $line "
		 echo " Use -f (force) to ignore this warning and rebuild anyway "
		 echo "===========================================================
"
		 cd "$2"						#Enter directory which contains currently downloaded tarball
		 rm "$3"						#And delete tarball
		 kill $$						#Kill parent process and exit
		 exit 1
		done
	 esle							#If file exists and -f was specified
		rm $fpath					#Delete existing file
		touch $fpath					#And create a new one
	fi
}


На FTP сервере выполняется скрипт для создания архивов с настройками:

#!/bin/sh
cdOrCreate()    #Enter the directory. Create if it's not exist, then enter. Arguments: 1) Path to directory (alternate or absolute).
{
        if ! [ -d $1 ]          #If directory does not exists
         then mkdir "$1"        #Then create it
        fi
        cd "$1"                 #Enter directory
}

cd /data/ftproot/FreeBSD/
cat list.txt | while read line
        do
         if [ "$1" == '-v' ]
          then
                echo "
== Processing subtree for version $line =="
         fi
         cdOrCreate $line
         rm tarball.tar.gz
         if [ "$1" == '-v' ]
          then
                tar -zcvf tarball.tar.gz *
                cd ..
          else
                tar -zcf tarball.tar.gz *
                cd ..
         fi
        done
echo "
DONE!"
if ! [ "$1" == '-v' ]
        then
                echo "Use -v for detailed output."
fi


В архив включаются все файлы, лежащие в поддиректориях, которые указаны в списке list.txt, т.е. в файле содержатся имена родительских директорий, соответствующих номеру версии, по одному в строке.
После распаковки архива скрипт проверяет ветки Merge и Replace. Для первой производится добавление или замена параметров в файлах конфигурации, в случае необходимости строки комментируются или раскомментируются. Для второй делается обычная замена файлов. Для каждого измененного файла сохраняется его MD5 в списке $DataDirectory$LockFile и, в случае повторных запусков скрипта, файлы с несоответствующим MD5 изменены не будут. Это было сделано для предотвращения отката изменений, сделанных администратором вручную.
Также, на случай предотвращения ошибочных изменений в скрипте сделана функция перезапуска через файл run.sh который создается, перезапускает скрипт и удаляется. В принципе — эту функцию легко выпилить.
Скрипт принимает следующие ключи (в любом порядке):
-f пропускает проверку на повторное применение архива
-s пропускает перезапуск скрипта
-d удаляет lockfile. Нужно для отката ручных изменений
-v VER принудительно указывает версию FreeBSD
Любой другой ключ вызовет функцию usage и скрипт завершит свою работу.
Также вы можете добавить свои варианты обработки поддиректорий в архиве. Для этого нужно описать их в файле parser.sh в ветке elif ниже соответствующего комментария.
Структура одного из моих архивов выглядит следующим образом:

Merge/boot/loader.conf
Merge/etc/rc.conf
Replace/usr/local/etc
Replace/usr/local/etc/svnup.conf
Replace/usr/share/skel
Replace/usr/share/skel/dot.cshrc
Replace/etc/ntp.conf
Replace/etc/adduser.conf
Replace/etc/portsnap.conf
Replace/root/.cshrc

Где после имени директории (у меня это Merge и Replace) сохраняется оригинальный путь к файлу. Имя директории и всё что находится до него убирается, далее файл обрабатывается кодом в соответствующей ему ветке if'а.
Скрипт написан с использованием только родных функций, т.е. будет работать на любой свежеустановленной фряхе.
Для запуска нужно сфетчить файл loader.sh, дать ему права на выполнение (chmod +x loared.sh) и, естественно, запустить.

Буду рад конструктивной критике, замечаниям и предложениям, т.к. прекрасно понимаю, что решение не идеально, и с удовольствием доработаю его.

P.S.: Очень извиняюсь за недописанный пост в пятницу. Случайно опубликовал копию и не заметил этого.
Tags:
Hubs:
+3
Comments 9
Comments Comments 9

Articles