xref: /freebsd/release/tools/vmimage.subr (revision 240c614d48cb0484bfe7876decdf6bbdcc99ba73)
1#!/bin/sh
2#
3#
4#
5# Common functions for virtual machine image build scripts.
6#
7
8scriptdir=$(dirname $(realpath $0))
9. ${scriptdir}/../scripts/tools.subr
10. ${scriptdir}/../../tools/boot/install-boot.sh
11
12export PATH="/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin"
13trap "cleanup" INT QUIT TRAP ABRT TERM
14
15# Platform-specific large-scale setup
16# Most platforms use GPT, so put that as default, then special cases
17PARTSCHEME=gpt
18ROOTLABEL="gpt"
19case "${TARGET}:${TARGET_ARCH}" in
20	powerpc:powerpc*)
21		PARTSCHEME=mbr
22		ROOTLABEL="ufs"
23		NOSWAP=yes # Can't label swap partition with MBR, so no swap
24	;;
25esac
26
27err() {
28	printf "${@}\n"
29	cleanup
30	return 1
31}
32
33cleanup() {
34	if [ -c "${DESTDIR}/dev/null" ]; then
35		umount_loop ${DESTDIR}/dev 2>/dev/null
36	fi
37
38	return 0
39}
40
41metalog_add_data() {
42	local file mode type
43
44	if [ -n "${NO_ROOT}" ]; then
45		file=$1
46		if [ -f ${DESTDIR}/${file} ]; then
47			type=file
48			mode=${2:-0644}
49		elif [ -d ${DESTDIR}/${file} ]; then
50			type=dir
51			mode=${2:-0755}
52		else
53			echo "metalog_add_data: ${file} not found" >&2
54			return 1
55		fi
56		echo "${file} type=${type} uname=root gname=wheel mode=${mode}" >> \
57		    ${DESTDIR}/METALOG
58	fi
59}
60
61vm_create_base() {
62
63	mkdir -p ${DESTDIR}
64
65	return 0
66}
67
68vm_copy_base() {
69	# Defunct
70	return 0
71}
72
73vm_base_packages_list() {
74	# Output a list of package sets equivalent to what we get from
75	# "installworld installkernel distribution", aka. the full base
76	# system.
77	for S in base kernels; do
78		echo FreeBSD-set-$S
79		echo FreeBSD-set-$S-dbg
80	done
81	case ${TARGET_ARCH} in
82	amd64 | aarch64 | powerpc64)
83		echo FreeBSD-set-lib32
84		echo FreeBSD-set-lib32-dbg
85	esac
86	echo FreeBSD-set-tests
87	# Also install pkg, since systems with a packaged base system should
88	# have the tools to upgrade themselves.
89	echo pkg
90}
91
92vm_extra_filter_base_packages() {
93	# Prototype. When overridden, allows further filtering of base system
94	# packages, reading package names from stdin and writing to stdout.
95	cat
96}
97
98vm_install_base() {
99	# Installs the FreeBSD userland/kernel to the virtual machine disk.
100
101	if [ -z "${NOPKGBASE}" ]; then
102		local pkg_cmd
103		pkg_cmd="${PKG_CMD} --rootdir ${DESTDIR} --repo-conf-dir ${PKGBASE_REPO_DIR}
104			-o ASSUME_ALWAYS_YES=yes -o IGNORE_OSVERSION=yes
105			-o ABI=${PKG_ABI} -o INSTALL_AS_USER=yes "
106		if [ -n "${NO_ROOT}" ]; then
107			pkg_cmd="$pkg_cmd -o METALOG=METALOG"
108		fi
109		$pkg_cmd update
110		selected=$(vm_base_packages_list | vm_extra_filter_base_packages)
111		$pkg_cmd install -U -r FreeBSD-base $selected
112		metalog_add_data ./var/db/pkg/local.sqlite
113		mkdir -p ${DESTDIR}/usr/local/etc/pkg/repos
114		echo 'FreeBSD-base: { enabled: yes }' > ${DESTDIR}/usr/local/etc/pkg/repos/FreeBSD.conf
115		metalog_add_data ./usr/local/etc/pkg/repos
116		metalog_add_data ./usr/local/etc/pkg/repos/FreeBSD.conf
117	else
118		cd ${WORLDDIR} && \
119			make DESTDIR=${DESTDIR} ${INSTALLOPTS} \
120			installworld installkernel distribution || \
121			err "\n\nCannot install the base system to ${DESTDIR}."
122	fi
123
124	# Bootstrap etcupdate(8) database.
125	mkdir -p ${DESTDIR}/var/db/etcupdate
126	etcupdate extract -B \
127		-M "TARGET=${TARGET} TARGET_ARCH=${TARGET_ARCH}" \
128		-s ${WORLDDIR} -d ${DESTDIR}/var/db/etcupdate \
129		-L /dev/stdout ${NO_ROOT:+-N}
130	if [ -n "${NO_ROOT}" ]; then
131		# Reroot etcupdate's internal METALOG to the whole tree
132		sed -n 's,^\.,./var/db/etcupdate/current,p' \
133		    ${DESTDIR}/var/db/etcupdate/current/METALOG | \
134		    env -i LC_COLLATE=C sort >> ${DESTDIR}/METALOG
135		rm ${DESTDIR}/var/db/etcupdate/current/METALOG
136	fi
137
138	echo '# Custom /etc/fstab for FreeBSD VM images' \
139		> ${DESTDIR}/etc/fstab
140	if [ "${VMFS}" != zfs ]; then
141		echo "/dev/${ROOTLABEL}/rootfs   /       ${VMFS}   rw,noatime      1       1" \
142			>> ${DESTDIR}/etc/fstab
143	fi
144	if [ -z "${NOSWAP}" ]; then
145		echo '/dev/gpt/swapfs  none    swap    sw      0       0' \
146			>> ${DESTDIR}/etc/fstab
147	fi
148	metalog_add_data ./etc/fstab
149
150	local hostname
151	hostname="$(echo $(uname -o) | tr '[:upper:]' '[:lower:]')"
152	echo "hostname=\"${hostname}\"" >> ${DESTDIR}/etc/rc.conf
153	metalog_add_data ./etc/rc.conf
154	if [ "${VMFS}" = zfs ]; then
155		echo "zfs_enable=\"YES\"" >> ${DESTDIR}/etc/rc.conf
156		echo "zpool_reguid=\"zroot\"" >> ${DESTDIR}/etc/rc.conf
157		echo "zpool_upgrade=\"zroot\"" >> ${DESTDIR}/etc/rc.conf
158		echo "kern.geom.label.disk_ident.enable=0" >> ${DESTDIR}/boot/loader.conf
159		echo "zfs_load=YES" >> ${DESTDIR}/boot/loader.conf
160		metalog_add_data ./boot/loader.conf
161	fi
162
163	return 0
164}
165
166vm_emulation_setup() {
167	if [ -n "${WITHOUT_QEMU}" ]; then
168		return 0
169	fi
170	if [ -n "${QEMUSTATIC}" ]; then
171		export EMULATOR=/qemu
172		cp ${QEMUSTATIC} ${DESTDIR}/${EMULATOR}
173	fi
174
175	mkdir -p ${DESTDIR}/dev
176	mount -t devfs devfs ${DESTDIR}/dev
177	chroot ${DESTDIR} ${EMULATOR} /bin/sh /etc/rc.d/ldconfig forcestart
178	cp /etc/resolv.conf ${DESTDIR}/etc/resolv.conf
179
180	return 0
181}
182
183vm_extra_install_base() {
184	# Prototype.  When overridden, runs extra post-installworld commands
185	# as needed, based on the target virtual machine image or cloud
186	# provider image target.
187
188	return 0
189}
190
191vm_extra_enable_services() {
192	if [ -n "${VM_RC_LIST}" ]; then
193		for _rcvar in ${VM_RC_LIST}; do
194			echo ${_rcvar}_enable="YES" >> ${DESTDIR}/etc/rc.conf
195		done
196	fi
197
198	if [ -z "${VMCONFIG}" -o -c "${VMCONFIG}" ]; then
199		echo 'ifconfig_DEFAULT="DHCP inet6 accept_rtadv"' >> \
200			${DESTDIR}/etc/rc.conf
201		# Expand the filesystem to fill the disk.
202		echo 'growfs_enable="YES"' >> ${DESTDIR}/etc/rc.conf
203	fi
204
205	return 0
206}
207
208vm_extra_install_packages() {
209	if [ -z "${VM_EXTRA_PACKAGES}" ]; then
210		return 0
211	fi
212	if [ -n "${NO_ROOT}" ]; then
213		for pkg in ${VM_EXTRA_PACKAGES}; do
214			INSTALL_AS_USER=yes \
215			${PKG_CMD} \
216			    -o ABI=${PKG_ABI} \
217			    -o METALOG=${DESTDIR}/METALOG.pkg \
218			    -o REPOS_DIR=${PKG_REPOS_DIR} \
219			    -o PKG_DBDIR=${DESTDIR}/var/db/pkg \
220			    -r ${DESTDIR} \
221			    install -y -r ${PKG_REPO_NAME} $pkg
222		done
223		INSTALL_AS_USER=yes \
224		${PKG_CMD} \
225		    -o ABI=${PKG_ABI} \
226		    -o REPOS_DIR=${PKG_REPOS_DIR} \
227		    -o PKG_DBDIR=${DESTDIR}/var/db/pkg \
228		    -r ${DESTDIR} \
229		    autoremove -y
230		if [ -n "${NOPKGBASE}" ]; then
231			metalog_add_data ./var/db/pkg/local.sqlite
232		fi
233	else
234		if [ -n "${WITHOUT_QEMU}" ]; then
235			return 0
236		fi
237
238		chroot ${DESTDIR} ${EMULATOR} env ASSUME_ALWAYS_YES=yes \
239			/usr/sbin/pkg bootstrap -y
240		for p in ${VM_EXTRA_PACKAGES}; do
241			chroot ${DESTDIR} ${EMULATOR} env ASSUME_ALWAYS_YES=yes \
242				/usr/sbin/pkg install -y ${p}
243		done
244		chroot ${DESTDIR} ${EMULATOR} env ASSUME_ALWAYS_YES=yes \
245		    /usr/sbin/pkg autoremove -y
246	fi
247
248	return 0
249}
250
251vm_extra_install_ports() {
252	# Prototype.  When overridden, installs additional ports within the
253	# virtual machine environment.
254
255	return 0
256}
257
258vm_extra_pre_umount() {
259	# Prototype.  When overridden, performs additional tasks within the
260	# virtual machine environment prior to unmounting the filesystem.
261
262	return 0
263}
264
265vm_emulation_cleanup() {
266	if [ -n "${WITHOUT_QEMU}" ]; then
267		return 0
268	fi
269
270	if ! [ -z "${QEMUSTATIC}" ]; then
271		rm -f ${DESTDIR}/${EMULATOR}
272	fi
273	rm -f ${DESTDIR}/etc/resolv.conf
274	umount_loop ${DESTDIR}/dev
275	return 0
276}
277
278vm_extra_pkg_rmcache() {
279	if [ -n "${NO_ROOT}" ]; then
280		${PKG_CMD} \
281		    -o ASSUME_ALWAYS_YES=yes \
282		    -o INSTALL_AS_USER=yes \
283		    -r ${DESTDIR} \
284		    clean -y -a
285	else
286		if [ -e ${DESTDIR}/usr/local/sbin/pkg ]; then
287			chroot ${DESTDIR} ${EMULATOR} env ASSUME_ALWAYS_YES=yes \
288			    /usr/local/sbin/pkg clean -y -a
289		fi
290	fi
291
292	return 0
293}
294
295buildfs() {
296	local md tmppool
297
298	# Copy entries from METALOG.pkg into METALOG, but first check to
299	# make sure that filesystem objects still exist; some things may
300	# have been logged which no longer exist if a package was removed.
301	if [ -f ${DESTDIR}/METALOG.pkg ]; then
302		while read F REST; do
303			if [ -e ${DESTDIR}/${F} ]; then
304				echo "${F} ${REST}" >> ${DESTDIR}/METALOG
305			fi
306		done < ${DESTDIR}/METALOG.pkg
307	fi
308
309	if [ -n "${NO_ROOT}" ]; then
310		# Check for any directories in the staging tree which weren't
311		# recorded in METALOG, and record them now.  This is a quick hack
312		# to avoid creating unusable VM images and should go away once
313		# the bugs which produce such unlogged directories are gone.
314		grep type=dir ${DESTDIR}/METALOG |
315		    cut -f 1 -d ' ' |
316		    sort -u > ${DESTDIR}/METALOG.dirs
317		( cd ${DESTDIR} && find . -type d ) |
318		    sort |
319		    comm -23 - ${DESTDIR}/METALOG.dirs > ${DESTDIR}/METALOG.missingdirs
320		if [ -s ${DESTDIR}/METALOG.missingdirs ]; then
321			echo "WARNING: Directories exist but were not in METALOG"
322			cat ${DESTDIR}/METALOG.missingdirs
323		fi
324		while read DIR; do
325			metalog_add_data ${DIR}
326		done < ${DESTDIR}/METALOG.missingdirs
327
328		if [ -z "${NOPKGBASE}" ]; then
329			# Add some database files which are created by pkg triggers;
330			# at some point in the future the tools which create these
331			# files should probably learn how to record them in METALOG
332			# (which would simplify no-root installworld as well).
333			metalog_add_data ./etc/login.conf.db
334			metalog_add_data ./etc/passwd
335			metalog_add_data ./etc/pwd.db
336			metalog_add_data ./etc/spwd.db 600
337			metalog_add_data ./var/db/services.db
338		fi
339
340		if [ -n "${MISSING_METALOGS}" ]; then
341			# Hack to allow VM configurations to add files which
342			# weren't being added to METALOG appropriately.  This
343			# is mainly a workaround for the @sample bug and it
344			# should go away before FreeBSD 15.1 ships.
345			for P in ${MISSING_METALOGS}; do
346				metalog_add_data ${P}
347			done
348		fi
349
350		# Sort METALOG file; makefs produces directories with 000 permissions
351		# if their contents are seen before the directories themselves.
352		env -i LC_COLLATE=C sort -u ${DESTDIR}/METALOG > ${DESTDIR}/METALOG.sorted
353		mv ${DESTDIR}/METALOG.sorted ${DESTDIR}/METALOG
354	fi
355
356	case "${VMFS}" in
357	ufs)
358		cd ${DESTDIR} && ${MAKEFS} ${MAKEFSARGS} -o label=rootfs -o version=2 -o softupdates=1 \
359			${VMBASE} .${NO_ROOT:+/METALOG}
360		;;
361	zfs)
362		cd ${DESTDIR} && ${MAKEFS} -t zfs ${MAKEFSARGS} \
363			-o poolname=zroot -o bootfs=zroot/ROOT/default -o rootpath=/ \
364			-o fs=zroot\;mountpoint=none \
365			-o fs=zroot/ROOT\;mountpoint=none \
366			-o fs=zroot/ROOT/default\;mountpoint=/\;canmount=noauto \
367			-o fs=zroot/home\;mountpoint=/home \
368			-o fs=zroot/tmp\;mountpoint=/tmp\;exec=on\;setuid=off \
369			-o fs=zroot/usr\;mountpoint=/usr\;canmount=off \
370			-o fs=zroot/usr/ports\;setuid=off \
371			-o fs=zroot/usr/src \
372			-o fs=zroot/usr/obj \
373			-o fs=zroot/var\;mountpoint=/var\;canmount=off \
374			-o fs=zroot/var/audit\;setuid=off\;exec=off \
375			-o fs=zroot/var/crash\;setuid=off\;exec=off \
376			-o fs=zroot/var/log\;setuid=off\;exec=off \
377			-o fs=zroot/var/mail\;atime=on \
378			-o fs=zroot/var/tmp\;setuid=off \
379			${VMBASE} .${NO_ROOT:+/METALOG}
380		;;
381	*)
382		echo "Unexpected VMFS value '${VMFS}'"
383		exit 1
384		;;
385	esac
386}
387
388umount_loop() {
389	DIR=$1
390	i=0
391	sync
392	while ! umount ${DIR}; do
393		i=$(( $i + 1 ))
394		if [ $i -ge 10 ]; then
395			# This should never happen.  But, it has happened.
396			echo "Cannot umount(8) ${DIR}"
397			echo "Something has gone horribly wrong."
398			return 1
399		fi
400		sleep 1
401	done
402
403	return 0
404}
405
406vm_create_disk() {
407	local BOOTFILES BOOTPARTSOFFSET FSPARTTYPE X86GPTBOOTFILE
408
409	if [ -z "${NOSWAP}" ]; then
410		SWAPOPT="-p freebsd-swap/swapfs::${SWAPSIZE}"
411	fi
412
413	if [ -n "${VM_BOOTPARTSOFFSET}" ]; then
414		BOOTPARTSOFFSET=":${VM_BOOTPARTSOFFSET}"
415	fi
416
417	if [ -n "${CONFIG_DRIVE}" ]; then
418		CONFIG_DRIVE="-p freebsd/config-drive::${CONFIG_DRIVE_SIZE}"
419	fi
420
421	case "${VMFS}" in
422	ufs)
423		FSPARTTYPE=freebsd-ufs
424		X86GPTBOOTFILE=i386/gptboot/gptboot
425		;;
426	zfs)
427		FSPARTTYPE=freebsd-zfs
428		X86GPTBOOTFILE=i386/gptzfsboot/gptzfsboot
429		;;
430	*)
431		echo "Unexpected VMFS value '${VMFS}'"
432		return 1
433		;;
434	esac
435
436	echo "Creating image...  Please wait."
437	echo
438
439	BOOTFILES="$(env TARGET=${TARGET} TARGET_ARCH=${TARGET_ARCH} \
440		WITH_UNIFIED_OBJDIR=yes \
441		make -C ${WORLDDIR}/stand -V .OBJDIR)"
442	BOOTFILES="$(realpath ${BOOTFILES})"
443	MAKEFSARGS="-s ${VMSIZE} -D"
444
445	case "${TARGET}:${TARGET_ARCH}" in
446		amd64:amd64 | i386:i386)
447			ESP=yes
448			BOOTPARTS="-b ${BOOTFILES}/i386/pmbr/pmbr \
449				   -p freebsd-boot/bootfs:=${BOOTFILES}/${X86GPTBOOTFILE}${BOOTPARTSOFFSET}"
450			ROOTFSPART="-p ${FSPARTTYPE}/rootfs:=${VMBASE}"
451			MAKEFSARGS="$MAKEFSARGS -B little"
452			;;
453		arm:armv7 | arm64:aarch64 | riscv:riscv64*)
454			ESP=yes
455			BOOTPARTS=
456			ROOTFSPART="-p ${FSPARTTYPE}/rootfs:=${VMBASE}"
457			MAKEFSARGS="$MAKEFSARGS -B little"
458			;;
459		powerpc:powerpc*)
460			ESP=no
461			BOOTPARTS="-p prepboot:=${BOOTFILES}/powerpc/boot1.chrp/boot1.elf -a 1"
462			ROOTFSPART="-p freebsd:=${VMBASE}"
463			if [ ${TARGET_ARCH} = powerpc64le ]; then
464				MAKEFSARGS="$MAKEFSARGS -B little"
465			else
466				MAKEFSARGS="$MAKEFSARGS -B big"
467			fi
468			;;
469		*)
470			echo "vmimage.subr: unsupported target '${TARGET}:${TARGET_ARCH}'" >&2
471			exit 1
472			;;
473	esac
474
475	if [ ${ESP} = "yes" ]; then
476		# Create an ESP
477		espfilename=$(mktemp /tmp/efiboot.XXXXXX)
478		make_esp_file ${espfilename} ${fat32min} ${BOOTFILES}/efi/loader_lua/loader_lua.efi
479		espsuffix=""
480		if [ -z "${BOOTPARTS}" ]; then
481			espsuffix="${BOOTPARTSOFFSET}"
482		fi
483		BOOTPARTS="${BOOTPARTS} -p efi/efiboot0:=${espfilename}${espsuffix}"
484
485		# Add this to fstab
486		mkdir -p ${DESTDIR}/boot/efi
487		echo "/dev/${ROOTLABEL}/efiboot0	/boot/efi       msdosfs     rw      2       2" \
488			>> ${DESTDIR}/etc/fstab
489	fi
490
491	# Add a marker file which indicates that this image has never
492	# been booted.  Some services run only upon the first boot.
493	touch ${DESTDIR}/firstboot
494	metalog_add_data ./firstboot
495
496	echo "Building filesystem...  Please wait."
497	buildfs
498
499	echo "Building final disk image...  Please wait."
500	${MKIMG} -s ${PARTSCHEME} -f ${VMFORMAT} \
501		${BOOTPARTS} \
502		${SWAPOPT} \
503		${CONFIG_DRIVE} \
504		${ROOTFSPART} \
505		-o ${VMIMAGE}
506
507	echo "Disk image ${VMIMAGE} created."
508
509	if [ ${ESP} = "yes" ]; then
510		rm ${espfilename}
511	fi
512
513	return 0
514}
515
516vm_extra_create_disk() {
517
518	return 0
519}
520