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 !

Пример использования CBSD/bhyve и ISC-DHCPD

Данная статья демонстрирует некоторые варианты использования CBSD master/pre/post хуков для интеграции окружений под CBSD с внешними сервисами.

Если прочесть описание функционала master/pre/post хуков, то данная возможность позволяет встравить выполнение пользовательских скриптов в различные стадии жизни виртуального окружения. При этом, в этих скриптах доступны значения переменных данного окружения, поэтому вы можете идентифицировать события с конкретным окружением и/или использовать эти параметры в каких-то своих целях.

Одна из задач, которые вам нужно решить при работе с виртуальными окружениями - это управление сетевыми настройками внутри виртуальной машины.

Это сделать не очень сложно, если у вас есть собственный образ виртуального окружения, в который вы можете встроить свои собственные скрипты (например, так работает vm_obtain метод в ClonOS), или работать с виртуальной машиной через virtio-console. Но этот метод не подходит, если:

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

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

Давайте попробуем на примере этой задачи показать, как можно использовать функционал master/pre/post хуков при работе с виртуальными окружениями, с использованием isc-dhcpd в связке с CBSD для управления IP адресами bhyve.

Вместо isc-dhcpd, может выступать любой другой сервер DHCP, например DNSMasq, схема будет примерно одинакова:

DHCPD сервер позволяет сконфигурировать сетевую подсеть. Кроме этого, вы можете фиксировать пары MAC адреса и IP. Другими словами, зная конкретный MAC адрес, вы конфигурируете DHCPD сервер выдавать ему фиксированный, заранее определенный IP адрес.

MAC адресами у нас заведует CBSD, поэтому наша цель - где-то на стадии запуска контейнера, сконфигурировать DHCP на выдачу IP адреса, который мы укажем в CBSD для MAC адреса, который выдаст (или мы сами назначим) данному виртуальному окружению.

Схематично, блок-схема работы должна получится у вас примерно такая:

Мы предполагаем, что CBSD у вас уже установлена и сконфигурирована, и ее версия не ниже 11.0.10

1) Установим пакет isd-dhcpd через pkg:

% pkg install net/isc-dhcp44-server

или из портов:

% env BATCH=yes make -C /usr/ports/net/isc-dhcp44-server install

2) Следующим шагом будет определение сети, которая будет использована в автоматической конфигурации и написании соответствующего блока конфигурационного файла для DHCP. Допустим, наша сеть будет 198.168.0.0/24, DNS будем брать от Google, а шлюз по-умолчанию будет 192.168.0.1:

Создадим каталог /root/etc:

% mkdir /root/etc

и поместим в каталоге файл /root/etc/dhcpd.conf с начальной конфигурацией. Она может выглядеть следующим образом:

option domain-name "example.com";
option domain-name-servers 8.8.8.8,8.8.4.4;
option subnet-mask 255.255.255.0;
default-lease-time 600;
max-lease-time 7200;

subnet 192.168.0.0 netmask 255.255.255.0 {
        range 192.168.0.2 192.168.0.254;
        option broadcast-address 192.168.0.255;
        option routers 192.168.0.1;
}

Этим мы обозначили сеть, за которую ответственнен dhcpd.

К сожалению, на момент написания этой статьи, конфигурационный файл dhcpd.conf не поддерживает возможность подключать отдельные конфигурационные файлы или указывать каталоги с экстра конфигурацией (либо автор плохо искал), поэтому нам необходим скрипт, который будет управлять второй частью конфигурационного файла dhcpd.conf, добавляя при запуске виртуальной машины запись соответствия IP адреса и MAC для первого сетевого интерфейса гвеста.

MAC адрес и IP адрес мы будем знать на этапе master/pre/post CBSD. Кроме того, чтобы у нас не возникало дубликатов с записями, записи к той или иной виртуальной машине идентифицируем каким-либо признаком (например, через комментарий), по которому будем находить старые записи и удалять перед добавлением новой. Для этой цели, нам также пригодится переменная $jname.

Итак, на входе мы будем получать три параметра - имя виртуального окружения, физический адрес (MAC) первого интерфейса и IP адрес. На выходе - добавим в /root/etc/dhcpd.conf привязку адреса и мака и перезагрузим dhcpd демон. В конце скрипта, перезагружаем dhcpd для применения новой конфигурации.

Пишем скрипт:

#!/bin/sh
DHCPD_CONF="/root/etc/dhcpd.conf"

# fatal error. Print message then quit with exitval
err() {
        exitval=$1
        shift
        echo "$*" 1>&2
        exit $exitval
}

[ -z "${jname}" ] && err 1 "no jname variable"
[ -z "${nic_hwaddr0}" ] && err 1 "no nic_hwaddr0 variable"
[ -z "${ip4_addr}" ] && err 1 "no ip4_addr variable"
[ ! -r "${DHCPD_CONF}" ] && err 1 "no ${DHCPD_CONF}"

# Remove old records for this host if exist
if grep "CBSD-AUTO-${jname}" ${DHCPD_CONF} >/dev/null 2>&1; then
	/bin/cp -a ${DHCPD_CONF} /tmp/dhcpd.tmp.$$
	trap "/bin/rm -f /tmp/dhcpd.tmp.$$" HUP INT ABRT BUS TERM EXIT
	grep -v "CBSD-AUTO-${jname}" /tmp/dhcpd.tmp.$$ > ${DHCPD_CONF}
fi

# Insert new records into config file
printf "host ${jname} { hardware ethernet ${nic_hwaddr0}; fixed-address ${ip4_addr}; } # CBSD-AUTO-${jname}\n" >> ${DHCPD_CONF}

service isc-dhcpd restart

И кладем его в каталог /root/bin под именем /root/bin/make_cbsd_dhcpd.sh:

Для того, чтобы DHCPD запускался вместе с системой, активируем его через rc.conf. Также, укажем альтернативный путь к конфигурационному файлу /root/etc/dhcpd.conf: Также, желательно указать интерфейс, на котором DHCPD будет обслуживать виртуальные машины. В моем случае, bhyve машины будут запускаться по-умолчанию - создавая tap интерфейс, который будет бриджеваться на uplink интерфейс. На моем сервере это сетевой интерфейс em0:

% sysrc dhcpd_enable="YES"
% sysrc dhcpd_ifaces="em0"
% sysrc dhcpd_conf="/root/etc/dhcpd.conf"

Внимание! Будьте аккуратны, если запускаете DHCPD на скоммутированном физическом интерфейсе. Поскольку ваш сервер будет выдавать адреса не только виртуальным окружениям bhyve, но также и всем остальным устройствам в вашей сети, если они их попросят. Более того, если в вашей сети есть еще один DHCPD сервер, вы также этим можете нарушить его работу.

Чтобы этого избежать, просто заблокируйте сетевой трафик от портов, по которым dhcpd во вне. Если интерфейс - em0, это можно например через ipfw:

/sbin/ipfw add 5000 deny tcp from me to any dst-port 67-68 via em0 out
/sbin/ipfw add 5001 deny udp from me to any dst-port 67-68 via em0 out

/sbin/ipfw add 5000 deny tcp from any to not me dst-port 67-68 via em0 in
/sbin/ipfw add 5001 deny udp from any to not me dst-port 67-68 via em0 in

Другой метод решения - вы можете указать dhcpd работать только на тех (tapX) интерфейсах, которые создаются при запуске виртуальной машины.

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

Нам потребуется хук master_prestart, поскольку действия нам необходимо выполнить на хостер системе и в момент, предшествующий непосредственно запуску виртуальной машины. Это директория master_prestart.d в системном персональном каталоге окружения: $workdir/jail-system/$jname

где:

  • $workdir - путь, с которым проинициализирована CBSD (обычно /usr/jails)
  • $jname - имя виртуального окружения. Например: freebsd1

Для обеспечения запуска хуков, нам достаточно положить выполняемый скрипт внутри соответствующего каталога. Это несложно, если у вас 1-2 постоянных окружения. Что делать, если вы планируете иметь большее количество виртуалок и более того, планируете достаточно часто удалять и создавать их? Мы должны будем всегда помнить о необходимости положить скрипт.

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

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

Создадим каталог /root/share, где будем хранить альтернативный контент system-каталогов для вновь создаваемых окружений:

% mkdir /root/share

Скопируем стандартное дерево каталогов из дистрибутива CBSD, оно не содержит никаких скриптов:

% cp -a ~cbsd/share/jail-system-default /root/share

Пожалуй, скрипт, добавляющий записи в dhcpd.conf у нас получился достаточно универсальный и нам не нужно копировать его в каждое создаваемое окружение. Мы обойдемся символическими ссылками, которые будут указывать на него. Для этого, зайдем в каталог master_prestart.d темплейта и укажем через ln на исходный скрипт:

% cd /root/share/jail-system-default/master_prestart.d
% ln -sf  /root/bin/make_cbsd_dhcpd.sh

И переназначим путь к skel-каталогу на нашу копию, через конфигурационный файл ~cbsd/etc/bhyve-default-default.conf, параметры в котором будут перезаписывать параметры файла ~cbsd/etc/defaults/bhyve-default-default.conf

echo 'systemskeldir="/root/share/jail-system-default"' > ~cbsd/etc/bhyve-default-default.conf

Вот и Все! Нам остается только запускать cbsd bconstruct-tui и штамповать виртуальные машины с указанными IP адресами в настройке ip4_addr.

Имейте ввиду, что на том интерфейсе, где вы запускаете DHCPD (в нашем случае это интерфейс em0, вы должны иметь хотя бы один IP адрес из той подсети, которую раздаете через DHCPD

Поскольку в нашем случае шлюз по-умолчанию является этот сервер, мы просто назначили 192.168.0.1 адрес в качестве алиаса на em0.



CBSD/bhyve и PXE boot: бездисковая загрузка bhyve виртуалок по сети

Начиная с версии 11.0.12, CBSD поддерживает возможность загрузки виртуальных машин bhyve через PXE протокол.

Для того, чтобы активировать загрузку bhyve по PXE, используйте меню vm_boot для выбора варианта 'net' boot device

Далее, нам необходимо будет доработать скрипт из первой части статьи таким образом, чтобы для некоторых серверов мы могли использовать дополнительные строчки конфигурации в dhcpd.conf, относящиеся к PXE/bootp протоколу загрузки. Такие как:

  filename "filename"
  option root-path "nfspath"

Для того, чтобы не "зашивать" параметры PXE индивидуально разным виртуальным машинам, давайте условимся иметь в системном каталоге окружения ( $workdir/jails-system/$jname ) некий файл, при наличие которого, все его содержимое будет перенесено as-is в блок:

   host $jname {
     ...
     // custom settings //
     ...
    
   }

Таким образом, делаем наш скрипт хука более гибким, поскольку мы теперь вправе вписывать любые dhcpd настройки касательно конкретной виртуальной машины в файл, который всегда лежит в иерархии системных каталогов данного окружения. Это позволяет нам не терять этот файл при операция клонирования, импортирования-экспортирования и так талее. Пускай это будет файл с именем: dhcpd_extra.conf

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

#!/bin/sh
# Helper for integration CBSD/bhyve and isc-dhcpd
# We assume bhyve have correct 'ip4_addr' settings
# To add dhcpf.conf extra-config (e.g. bootp-related) for
#    hosts ${jname} {  ..  }
#    please use file in ${jailsysdir}/${jname}/dhcpd_extra.conf
#    << all content from this file will be dropped to { } block
#
DHCPD_CONF="/root/etc/dhcpd.conf"

. /etc/rc.conf

workdir="${cbsd_workdir}"

set -e
. ${workdir}/cbsd.conf
. ${workdir}/nc.subr
set +e

export NOCOLOR=1

# $1 - ip test
# return 0 if ip4
# return 1 if ip6
# return 2 if DHCP
# return 3 if unknown
ip_type()
{
	case "${1}" in
		*\.*\.*\.*)
			return 0
			;;
		*:*)
			return 1
			;;
		[Dd][Hh][Cc][Pp])
			return 2
			;;
		*)
			return 3
			;;
	esac
}

[ -z "${jname}" ] && err 1 "no jname variable"
[ -z "${nic_hwaddr0}" ] && err 1 "no nic_hwaddr0 variable"
[ -z "${ip4_addr}" ] && err 1 "no ip4_addr variable"
[ ! -r "${DHCPD_CONF}" ] && err 1 "no ${DHCPD_CONF}"

EXTRA_CONF="${jailsysdir}/${jname}/dhcpd_extra.conf"

ip_type ${ip4_addr}

ret=$?

case "${ret}" in
	0)
		echo "IPv4 detected"
		;;
	1)
		echo "IPv6 detected"
		;;
	2)
		# Fake DHCP - we need learn to get IPS from real dhcpd (tap iface + mac ?)
		tmp_addr=$( /usr/local/bin/cbsd dhcpd )
		ip4_addr=${tmp_addr%%/*}
		# check again
		ip_type ${ip4_addr}
		ret=$?
		case "${ret}" in
			0|1)
				# update new IP
				cbsd bset ip4_addr="${ip4_addr}" jname="${jname}"
				;;
			*)
				err 0 "Can't obtain DHCP addr"
				;;
		esac
		;;
	*)
		err 0 "Unknown IP type: ${ip4_addr}"
		;;
esac

# Remove old records for this host if exist
if grep "CBSD-AUTO-${jname}" ${DHCPD_CONF} >/dev/null 2>&1; then
	/bin/cp -a ${DHCPD_CONF} /tmp/dhcpd.tmp.$$
	trap "/bin/rm -f /tmp/dhcpd.tmp.$$" HUP INT ABRT BUS TERM EXIT
	grep -v "CBSD-AUTO-${jname}" /tmp/dhcpd.tmp.$$ > ${DHCPD_CONF}
fi

# Insert new records into config file
cat >> ${DHCPD_CONF} <<EOF
host ${jname} {					# CBSD-AUTO-${jname}
	hardware ethernet ${nic_hwaddr0};	# CBSD-AUTO-${jname}
	fixed-address ${ip4_addr};		# CBSD-AUTO-${jname}
EOF

if [ -r "${EXTRA_CONF}" ]; then
	echo "Found extra conf: ${EXTRA_CONF}"
	cat ${EXTRA_CONF} |while read _line; do
		cat >> ${DHCPD_CONF} <<EOF
	${_line}				# CBSD-AUTO-${jname}
EOF
	done
fi

cat >> ${DHCPD_CONF} <<EOF
}				# CBSD-AUTO-${jname}
EOF

arp -d ${ip4_addr}
arp -s ${ip4_addr} ${nic_hwaddr0} pub

service isc-dhcpd restart

И кладем этот скрипт вместо старого /root/bin/make_cbsd_dhcpd.sh

Теперь, нам остается создать файл dhcpd_extra.conf в тех системных каталогах виртуальных машин, которые будут загружаться по PXE, где главную роль будут играть такие параметры, как:

     filename "FreeBSD/install/boot/pxeboot" ;
     option root-path "192.168.0.1:/b/tftpboot/FreeBSD/install/" ;

Остальная настройка - это тривиальная конфигурация tftpboot и NFS сервисов, отдающие нужные вам файлы

Вы можете найти большое количество статей о конфигурировании PXE сервера в интернете, а также на официальном сайте FreeBSD: the Handbook entry on diskless booting.

Либо, вы можете сделать это по шагам ниже. Эти команды использовались для создания PXE загрузки, продемонстрированное в этом коротком ролике: Bhyve unattended installation with CBSD: PXE and cloud-init

В данном примере, наш сервер имеет IP адрес: 192.168.0.2 и мы загружаем по PXE окружение, скопированное с инсталляционного ISO образа FreeBSD 11.0-R

% mkdir /FreeBSD
% wget https://download.freebsd.org/ftp/releases/amd64/amd64/ISO-IMAGES/11.0/FreeBSD-11.0-RELEASE-amd64-disc1.iso
% mount_cd9660 /dev/`mdconfig -a -t vnode -f ./FreeBSD-11.0-RELEASE-amd64-disc1.iso` /mnt
% cp -a /mnt/* /FreeBSD/
% umount /mnt
% mkdir /root/etc
% cat > /root/etc/inetd.conf <<EOF
tftp   dgram   udp     wait    root    /usr/libexec/tftpd      tftpd -l -s /FreeBSD
tftp   dgram   udp6    wait    root    /usr/libexec/tftpd      tftpd -l -s /FreeBSD
EOF

% echo "/FreeBSD -ro -alldirs" > /etc/exports
% echo "192.168.0.2:/FreeBSD /mnt nfs rw 0 0" > /FreeBSD/etc/fstab

% sysrc inetd_enable="YES"
% sysrc inetd_flags="-wW -C 60 /root/etc/inetd.conf"
% sysrc nfs_server_enable="YES"
% sysrc mountd_enable="YES"

% service nfsd restart
% service mountd restart
% service inetd restart

На этом загрузка bhyve по PXE готова. Остается создать виртуальную машину, в 'cbsd bconfig' выбрать метод загрузки 'net'

И написать extra-настройки для данной виртуальной машины в файле dhcpd_extra.conf

Например, если $workdir в вашей системе - /usr/jails, а вы конфигурируете PXE загрузку виртуальной машины с именем freebsd1, то в каталоге /usr/jails/jails-system/freebsd1 должен лежать файл dhcpd_extra.conf с таким содержимым:

     filename "loader.efi" ;
     option root-path "192.168.0.2:/FreeBSD/" ;