FreeBSD virtual environment management and repository

2020-10 upd: we reached the first fundraising goal and rented a server in Hetzner for development! Thank you for donating !

Сборка своих репозиториев ПО для FreeBSD через CBSD cpr

Общая информация

Эта статья внезапно устарела. Обновленная версия: https://forums.bsdstore.ru/viewtopic.php?t=7


//Draft, unformatted

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

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

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

Хорошо, когда для решения этой задачи используется резервное копирование или, что гораздо практичнее, использовать снэпшот файловой системы: например, перед работами сделать ZFS snapshot для клетки, и при любых проблемах после обновления в течении секунды вернуть состояние обратно.

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

Кроме этого, сборочная ферма может быть одна, а обслуживать она в состоянии не одну сотню систем

Когда я начинал писать cbsd cpr, уже давно были известны аналогичные функционалы в виде TinderBox и Poudriere, однако в каждом из этих решений мне чего-то не хватало.

Требования, которые предьявляла моя задача:

  • Сборка производится по заранее сгенерированныму списку ПО.
  • Сборка каждого отдельного репозитория/версии производится в изолированном окружении (включая собственный кеш ccache (опционально) для компиляции, который накапливается при каждых повторных сборках в пределах версии и ветки);
  • При сборке окружения, используется база и ядро, используемые CBSD, соответственно, при необходимости иметь более свежую версию базы или ядра, можно воспользоваться cbsd repo для получения объектов из репозитория, или cbsd svnup/buildworld/kernel* для сборки с нуля.
  • Каждый набор ПО (или один и тот же список) может использовать свою собственную версию опций портов (/var/db/ports) и персональный make.conf файл и иметь именованный репозиторий, например в таком виде:
    • nox — WITHOUT_X11, набор ПО для серверного окружения, без поддержки X — этот набор ПО используется на production-серверах.
    • devnox — тот же набор ПО и опция WITHOUT_X11, но софт собран с дебагом и поддержкой DTRACE; этот набор ПО используется на dev-клетках, где идет разработка и профилирование проектов.
    • xorg — X11-based GUI приложения. Данный набор содержит xorg, kde, gnome, мультимедия, офисные приложения, IDE для разработки и прочий инструментарий, характерный для рабочих станций.
    • otherver — какой-то альтернативный набор опций или версии ПО, который могут предпочесть пользователи хостинга.
  • Иметь возможность использовать больше одного билдера ПО, в этом случае сценарии репозитория и опции портов реплицировалась (например через csync2) на соответствующие билдеры: если в процессе сборки репозитория nox вы выбрали некоторые опции для одного порта, то собирая тот же репозиторий nox на другом сервере или другой версии, вывода окна со списом опций уже не будет — он синхронизируется с сервера, которая прошла этот путь первой.
  • Иметь возможность задействовать для компиляции свободные ресурсы серверов через distcc. В этом случае, на нужных серверах скачивалась и запускалась клетка distcc с минимальным nice, что при сборке не мешало работать остальным сервисам.
  • Иметь версионность каталогов, куда сохраняются новые сборки. В один момент времени только один сет является активным, переключение происходит сменой симлинка. Кроме этого, при наличии нескольких download серверов, требуется время для полной синхронизации, поэтому проливка осуществляется в неактивную (slave) зону и по факту полной сихронизации, одномоментно переключается.
  • Конечные хосты забирают готовые пекеджи не с билдеров, а с соответствующих downloads-серверов, которых может быть несколько. Таким образом, зеркала отдающие пекеджи, не зависят от здоровья самих билдеров.
  • Протоколирование работы скрипта ведется в /tmp каталоге chroot окружения:

    • — build.log — общий лог процесса сборки
    • — packages.log — лог на этапе pkg create
    • — port_log* — временные файлы лога компиляции индивидуального порта, удаляющиеся автоматически при успешной сборке порта
  • Каждая версия репозитория может иметь свой собственный E-mail для уведомлений о начале-конце сборки.
  • Каждую сборку можно запустить с параметром pause=1, что по окончанию сборки система будет ожидать нажатия клавиши. А пока это не сделано — можно зайти в chroot окружение, просмотреть проблемы и исправить их вручную.
  • порты монтируются в chroot окружение из мастер системы, либо каждое окружение имеет свою копию портов.

Схематичный рисунок сборочной фермы:

Для комфортной работы напишем переключатель версионности каталогов.

Принцип работы данного скрипта простая. Допустим, у нас есть локация /home/web/pkg.bsdstore.ru/ для хранения пекеджей, где:

/home/web/pkg.bsdstore.ru/master — это SymbolicLink на активный set с билдами. Этот же путь является Docroot для WEB сервера и его имя никогда не меняется.

/home/web/pkg.bsdstore.ru/1 — первый физический каталог, содержащий один набор данных

/home/web/pkg.bsdstore.ru/2 — второй физический каталог, содержащий предыдущую (или следующую) версию набора данных

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

		#!/bin/sh
		# When use with config file, follow variable must be set, sample:
		# _PATH="/usr/symlinks"
		# _RANGE="1 2 3"
		# _MASTER_LINK="master"
		# _SLAVE_LINK="slave"
		# _ACTION="next"
		# _X=1  #if action=set
		# _Y=2  #if action=set
		#
		# or by command line:
		# ./symlink_changer -p /usr/symlinks -r "1 2 3" -m master -s slave -a next
		#

		err() {
			exitval=$1
			shift
			echo "$*" 1>&2
			exit $exitval
		}

		get_symlink() {
			local _res

			[ -z $1 ] && return 1
			_res=`readlink ${_PATH}/${1}`
			[ $? -ne 0 ] && return 1
			printf `basename ${_res}`
		}

		get_next() {
			local _cur _test _first _count

			_test="${_PATH}/${1}"
			_cur=0
			_count=0

			for i in ${_RANGE}; do
				[ ${_count} -eq 0 ] && _first=${i}  ## store first element
				[ ${_cur} -eq 1 ] && printf ${i} && return 0
				[ "${_PATH}/$i" = "${_test}" ] && _cur=1
				_count=$(( _count + 1 ))
			done

			printf ${_first}
		}

		get_prev() {
			local _cur _test _first _count _REVSLOTS
			_test="${_PATH}/${1}"
			_cur=0
			_count=0
			_REVSLOTS=`echo ${_RANGE} | tr " " "\n" |sort -r`

			for i in ${_REVSLOTS}; do
				[ ${_count} -eq 0 ] && _first=${i}  ## store first element
				[ ${_cur} -eq 1 ] && printf ${i} && return 0
				[ "${_PATH}/$i" = "${_test}" ] && _cur=1
				_count=$(( _count + 1 ))
			done

			printf ${_first}
		}
		# create or change new layout by
		# $1 - new dir for master link
		# $2 - new dir for slave
		sym_action() {
			local _masterdir _slavedir
			[ -z "${1}" -o -z "${2}" ] && return 1

			_masterdir="${_PATH}/${1}"
			_slavedir="${_PATH}/${2}"

			[ ! -d "${_masterdir}" ] && mkdir "${_masterdir}"
			[ ! -d "${_slavedir}" ] && mkdir "${_slavedir}"

			# ln -sf not work correctly here - create symlink in old master folder
			cd ${_PATH}
			rm -f "${_PATH}/${_MASTER_LINK}" && /bin/ln -s "${1}" "${_MASTER_LINK}"
			rm -f "${_PATH}/${_SLAVE_LINK}" && /bin/ln -s "${2}" "${_SLAVE_LINK}"
		}

		usage() {
			echo "$0 -c confpath -p path -r \"range\" -m masterlink_name -s slavelink_name -a action [-x dir1] [-y dir2]"
			echo "action must be: next, prev, set"
			echo "when action = set, x/y = is new masterdir/slavedir"
			exit
		}


		# MAIN()
		while getopts "c:p:r:m:s:a:x:y:" opt; do
				case "$opt" in
				c) _conf="$OPTARG" ;;
				p) _path="$OPTARG" ;;
				r) _range="$OPTARG" ;;
				m) _master_link="$OPTARG" ;;
				s) _slave_link="$OPTARG" ;;
				a) _action="$OPTARG" ;;
				x) _x="$OPTARG" ;;
				y) _y="$OPTARG" ;;
				*) usage ;;
				esac
				shift $(($OPTIND - 1))
		done

		[ -n "${_conf}" -a -f "${_conf}" ] && . ${_conf}

		[ -n "${_path}" ] && _PATH=${_path}
		[ -n "${_range}" ] && _RANGE=${_range}
		[ -n "${_master_link}" ] && _MASTER_LINK=${_master_link}
		[ -n "${_slave_link}" ] && _SLAVE_LINK=${_slave_link}
		[ -n "${_action}" ] && _ACTION=${_action}
		[ -n "${_x}" ] && _X=${_x}
		[ -n "${_y}" ] && _Y=${_y}

		[ -z "${_ACTION}" ] && err 1 "Give me action"
		[ -z "${_PATH}" -o -z "${_MASTER_LINK}" -o -z "${_SLAVE_LINK}" -o -z "${_RANGE}" ] && err 1 "not all neccesary variable has been set"
		cd ${_PATH} || err 1 "Cant cwd to ${_PATH}"

		# init area
		_curmaster=`eval get_symlink ${_MASTER_LINK}`
		_curslave=`eval get_symlink ${_SLAVE_LINK}`

		case "${_ACTION}" in
		"next")
				_master=`eval get_next ${_curmaster}`
				_slave=`eval get_next ${_master}`
				;;
		"prev")
				_master=`eval get_prev ${_curmaster}`
				_slave=${_curmaster}
				;;
		"set")
				_master="${_X}"
				_slave="${_Y}"
				;;
		*)
				err 1 "No action set"
				;;
		esac

		sym_action ${_master} ${_slave}
		

Howto: Работа с cpr скриптом

Текущее расположение скриптов на GitHub.

Клонирем скрипты например в домашний каталог пользователя root:

		% cd /root
		% git clone https://github.com/olevole/cbsd-cpr.git
		

Создаем файл списка ПО, которое cpr должен собрать. Назовем файл task1.list и наполним его например таким содержимым:

/usr/ports/databases/mariadb55-server
/usr/ports/databases/postgresql93-server
/usr/ports/lang/php55
/usr/ports/www/nginx-devel
/usr/ports/misc/mc
		

Если вы создадите файл с имя_проекта_make.conf, данный файл будет использоваться как /etc/make.conf при сборке репозитория. Например, вы можете сразу определить несколько KNOBS-ов, дабы они применялись на все опции портов. В нашем примере, создадим файл task1_make.conf с таким содержимым:

		OPTIONS_UNSET+= NLS
		OPTIONS_UNSET+= EXAMPLES
		OPTIONS_UNSET+= DOCS
		OPTIONS_UNSET+= MAN
		OPTIONS_UNSET+= X11
		
		OPTIONS_SET+= PKGNG
		WITH_BDB_VER=6
		DEFAULT_VERSIONS=php=55
		

Что равносильно WITHOUT_NSS=yes, WITHOUT_EXAMPLES=yes, WITHOUT_DOCS=yes, WITHOUT_MAN=yes, WITHOUT_X11=yes

Запуск скрипта:

		% ./build -v версия_базы -m имя_задачи> -r имя_группы_опций -n1 -p1
		

Где:

  • Версия базы - версия базы FreeBSD для которой необходимо собрать пекеджи. Например: -v 10.0
  • Имя задачи - имя файла list. Например: -m task1
  • Имя группы опций - обычно совпадает с именем задачи. Но, если у вас много задач и для всех подходят одни и те же опции, вы можете использовать одну группу опций. Данные опции сохраняются в каталоге ${workdir}/var/db/ports-имя и при отработке cpr монтируются через nullfs в /var/db/ports внутри chroot окружения. Например: -r task1. Внимание! Если вы допустили ошибку и в спешке выбрали не ту опцию, просто зайдите в каталог ${workdir}/var/db/ports/ports-task1 и сотрите соответствующий каталог для появления окна опций в следующей сборке.
  • -n: Признак нового репозитория или продолжения. Если -n1, то cpr создаст chroot окружение и будет собирать все с нуля. Если -n0, то в режиме продолжения. Второй вариант удобен, если первичная сборка закончилась неудачей и хочется ее повторить. При этом, те порты что уже были собраны в первом заходе, собираться не будут. Внимание: после каждого обновления дерева портов необходим запуск в режиме -n1, иначе ранее собранные версии портов могут быть уже устаревшими
  • -p: Сделать ли паузу до нажатия клавиши перед тем, как начать собирать пекеджи. Это дает возможность зайти в chroot и поправить проблемные порты вручную, если таковый имеются

В описываемом примере, запуск производим следующей строчкой

		% ./build -v 10.0 -m task1 -r task1 -n1 -p1
		

Вначале будет выполнен make config-recursive для выбора всех опций учавствующего в сборке ПО для того, чтобы сам процесс сборки прошел без интерактивного участия, когда опция может всплыть у какого-то порта где-то в середине работы.

В редких случаях ввиду ошибок в зависимостях, вы можете увидеть зацикливание при отработке config. Если вы видете, что один и тот же порт постоянно повторяется, нажмите 1 раз Ctrl+C для выхода из этого цикла

По факту сборки по, если вы запускали с паузой, будет выведено сообщение

 ...
 Working on /usr/ports/misc/mc. 1 ports left. 
 Pause before create pkg.
 You can enter in chroot via: chroot /root/jails/tmp/task1-10.0
 Press any key to continue
		

Вы можете зайти в chroot и проверить наличие файлов port_log*. Если их нет, сборка прошла успешно. Если они есть, вы можете посмотреть из-за чего тот или иной порт не собрался

Скрипт cpr по окончанию работы автоматически проверит наличие этих файлов и если ошибки есть, содержимое файлов будет выслано на email указанный в $workdir/etc/defaults/cpr.conf (параметр CBSDCPR) и выведено в консоль

Если же ошибок нет (или вы в ручную поправили ошибки в chroot, собрали софт и удалили файлы port_log*), то скрипт создаст пекеджи, которые затем можно раздавать через WEB сервер или использовать локально через схему file:/// в pkg.conf для локального обновления

Путь на который настроены скрипты для генерации .txz: указывается через парамет dstdir= (смотрите внутрь скрипта build)