Добрый час, Хабровчане!
Хочу поделиться своим опытом автоматизации процесса установки и настройки 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.: Очень извиняюсь за недописанный пост в пятницу. Случайно опубликовал копию и не заметил этого.