xref: /openbsd/usr.sbin/sysmerge/sysmerge.sh (revision e5dd7070)
1#!/bin/ksh -
2#
3# $OpenBSD: sysmerge.sh,v 1.235 2019/09/28 17:30:07 ajacoutot Exp $
4#
5# Copyright (c) 2008-2014 Antoine Jacoutot <ajacoutot@openbsd.org>
6# Copyright (c) 1998-2003 Douglas Barton <DougB@FreeBSD.org>
7#
8# Permission to use, copy, modify, and distribute this software for any
9# purpose with or without fee is hereby granted, provided that the above
10# copyright notice and this permission notice appear in all copies.
11#
12# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
13# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
14# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
15# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
16# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
17# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
18# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19#
20
21umask 0022
22export PATH=/usr/bin:/bin:/usr/sbin:/sbin
23
24usage() {
25	echo "usage: ${0##*/} [-bdp]" >&2 && exit 1
26}
27
28# OpenBSD /etc/rc v1.456
29stripcom() {
30	local _file=$1 _line
31
32	[[ -s $_file ]] || return
33
34	while read _line ; do
35		_line=${_line%%#*}
36		[[ -n $_line ]] && print -r -- "$_line"
37	done <$_file
38}
39
40sm_error() {
41	(($#)) && echo "!!!! $@"
42	rm -rf ${_TMPROOT}
43	exit 1
44}
45
46sm_trap() {
47	rm -f /var/sysmerge/{etc,pkg,xetc}sum
48	sm_error
49}
50
51trap "sm_trap" 1 2 3 13 15
52
53sm_info() {
54	(($#)) && echo "---- $@" || true
55}
56
57sm_warn() {
58	(($#)) && echo "**** $@" || true
59}
60
61sm_extract_sets() {
62	${PKGMODE} && return
63	local _e _x _set
64
65	[[ -f /var/sysmerge/etc.tgz ]] && _e=etc
66	[[ -f /var/sysmerge/xetc.tgz ]] && _x=xetc
67	[[ -z ${_e}${_x} ]] && sm_error "cannot find sets to extract"
68
69	for _set in ${_e} ${_x}; do
70		tar -xzphf \
71			/var/sysmerge/${_set}.tgz || \
72			sm_error "failed to extract ${_set}.tgz"
73	done
74}
75
76sm_rotate_bak() {
77	local _b
78
79	for _b in $(jot 4 3 0); do
80		[[ -d ${_BKPDIR}.${_b} ]] && \
81			mv ${_BKPDIR}.${_b} ${_BKPDIR}.$((_b+1))
82	done
83	rm -rf ${_BKPDIR}.4
84	[[ -d ${_BKPDIR} ]] && mv ${_BKPDIR} ${_BKPDIR}.0
85	# make sure this function is only run _once_ per sysmerge invocation
86	unset -f sm_rotate_bak
87}
88
89# get pkg @sample information
90exec_espie() {
91	local _tmproot
92
93	_tmproot=${_TMPROOT} /usr/bin/perl <<'EOF'
94use strict;
95use warnings;
96
97package OpenBSD::PackingElement;
98
99sub walk_sample
100{
101}
102
103package OpenBSD::PackingElement::Sampledir;
104sub walk_sample
105{
106	my $item = shift;
107	print "0-DIR", " ",
108	      $item->{owner} // "root", " ",
109	      $item->{group} // "wheel", " ",
110	      $item->{mode} // "0755", " ",
111	      $ENV{'_tmproot'}, $item->fullname,
112	      "\n";
113}
114
115package OpenBSD::PackingElement::Sample;
116sub walk_sample
117{
118	my $item = shift;
119	print "1-FILE", " ",
120	      $item->{owner} // "root", " ",
121	      $item->{group} // "wheel", " ",
122	      $item->{mode} // "0644", " ",
123	      $item->{copyfrom}->fullname, " ",
124	      $ENV{'_tmproot'}, $item->fullname,
125	      "\n";
126}
127
128package main;
129use OpenBSD::PackageInfo;
130use OpenBSD::PackingList;
131
132for my $i (installed_packages()) {
133	my $plist = OpenBSD::PackingList->from_installation($i);
134	$plist->walk_sample();
135}
136EOF
137}
138
139sm_cp_pkg_samples() {
140	! ${PKGMODE} && return
141	local _install_args _i _ret=0 _sample
142
143	# access to full base system hierarchy is implied in packages
144	mtree -qdef /etc/mtree/4.4BSD.dist -U >/dev/null
145	mtree -qdef /etc/mtree/BSD.x11.dist -U >/dev/null
146
147	# @sample directories are processed first
148	exec_espie | sort -u | while read _i; do
149		set -A _sample -- ${_i}
150		_install_args="-o ${_sample[1]} -g ${_sample[2]} -m ${_sample[3]}"
151		if [[ ${_sample[0]} == "0-DIR" ]]; then
152			install -d ${_install_args} ${_sample[4]} || _ret=1
153		else
154			# directory we want to copy the @sample file into
155			# does not exist and is not a @sample so we have no
156			# knowledge of the required owner/group/mode
157			# (e.g. /var/www/usr/sbin in mail/femail,-chroot)
158			_pkghier=${_sample[5]%/*}
159			if [[ ! -d ${_pkghier#${_TMPROOT}} ]]; then
160				sm_warn "skipping ${_sample[5]#${_TMPROOT}}: ${_pkghier#${_TMPROOT}} does not exist"
161				continue
162			else
163				# non-default prefix (e.g. mail/roundcubemail)
164				install -d ${_pkghier}
165			fi
166			install ${_install_args} \
167				${_sample[4]} ${_sample[5]} || _ret=1
168		fi
169	done
170
171	if [[ ${_ret} -eq 0 ]]; then
172		find . -type f -exec sha256 '{}' + | sort \
173			>./var/sysmerge/pkgsum || _ret=1
174	fi
175	[[ ${_ret} -ne 0 ]] && \
176		sm_error "failed to populate packages @samples and create sum file"
177}
178
179sm_run() {
180	local _auto_upg _c _c1 _c2 _cursum _diff _i _k _j _cfdiff _cffiles
181	local _ignorefiles _cvsid1 _cvsid2 _matchsum _mismatch
182
183	sm_extract_sets
184	sm_add_user_grp
185	sm_cp_pkg_samples
186
187	for _i in etcsum xetcsum pkgsum; do
188		if [[ -f /var/sysmerge/${_i} && \
189			-f ./var/sysmerge/${_i} ]] && \
190			! ${DIFFMODE}; then
191			# redirect stderr: file may not exist
192			_matchsum=$(sha256 -c /var/sysmerge/${_i} 2>/dev/null | \
193				sed -n 's/^(SHA256) \(.*\): OK$/\1/p')
194			# delete file in temproot if it has not changed since
195			# last release and is present in current installation
196			for _j in ${_matchsum}; do
197				# skip sum files
198				[[ ${_j} == ./var/sysmerge/${_i} ]] && continue
199				[[ -f ${_j#.} && -f ${_j} ]] && \
200					rm ${_j}
201			done
202
203			# set auto-upgradable files
204			_mismatch=$(diff -u ./var/sysmerge/${_i} /var/sysmerge/${_i} | \
205				sed -n 's/^+SHA256 (\(.*\)).*/\1/p')
206			for _k in ${_mismatch}; do
207				# skip sum files
208				[[ ${_k} == ./var/sysmerge/${_i} ]] && continue
209				# compare CVS Id first so if the file hasn't been modified,
210				# it will be deleted from temproot and ignored from comparison;
211				# several files are generated from scripts so CVS ID is not a
212				# reliable way of detecting changes: leave for a full diff
213				if ! ${PKGMODE} && \
214					[[ ${_k} != ./etc/@(fbtab|ttys) && \
215					! -h ${_k} ]]; then
216					_cvsid1=$(sed -n "/[$]OpenBSD:.*Exp [$]/{p;q;}" ${_k#.} 2>/dev/null)
217					_cvsid2=$(sed -n "/[$]OpenBSD:.*Exp [$]/{p;q;}" ${_k} 2>/dev/null)
218					[[ -n ${_cvsid1} ]] && \
219						[[ ${_cvsid1} == ${_cvsid2} ]] && \
220						[[ -f ${_k} ]] && rm ${_k} && \
221						continue
222				fi
223				# redirect stderr: file may not exist
224				_cursum=$(cd / && sha256 ${_k} 2>/dev/null)
225				grep -q "${_cursum}" /var/sysmerge/${_i} && \
226					! grep -q "${_cursum}" ./var/sysmerge/${_i} && \
227					_auto_upg="${_auto_upg} ${_k}"
228			done
229			[[ -n ${_auto_upg} ]] && set -A AUTO_UPG -- ${_auto_upg}
230		fi
231		[[ -f ./var/sysmerge/${_i} ]] && \
232			mv ./var/sysmerge/${_i} /var/sysmerge/${_i}
233	done
234
235	# files we don't want/need to deal with
236	_ignorefiles="/etc/group
237		      /etc/localtime
238		      /etc/master.passwd
239		      /etc/motd
240		      /etc/passwd
241		      /etc/pwd.db
242		      /etc/spwd.db
243		      /var/db/locate.database
244		      /var/mail/root"
245	# in case X(7) is not installed, xetcsum is not removed by the loop above
246	_ignorefiles="${_ignorefiles} /var/sysmerge/xetcsum"
247	[[ -f /etc/sysmerge.ignore ]] && \
248		_ignorefiles="${_ignorefiles} $(stripcom /etc/sysmerge.ignore)"
249	for _i in ${_ignorefiles}; do
250		rm -f ./${_i}
251	done
252
253	# aliases(5) needs to be handled last in case mailer.conf(5) changes
254	_c1=$(find . -type f -or -type l | grep -v '^./etc/mail/aliases$')
255	[[ -f ./etc/mail/aliases ]] && _c2="./etc/mail/aliases"
256	for COMPFILE in ${_c1} ${_c2}; do
257		IS_BIN=false
258		IS_LINK=false
259		TARGET=${COMPFILE#.}
260
261		# links need to be treated in a different way
262		if [[ -h ${COMPFILE} ]]; then
263			IS_LINK=true
264			[[ -h ${TARGET} && \
265				$(readlink ${COMPFILE}) == $(readlink ${TARGET}) ]] && \
266				rm ${COMPFILE} && continue
267		elif [[ -f ${TARGET} ]]; then
268			# empty files = binaries (to avoid comparison);
269			# only process them if they don't exist on the system
270			if [[ ! -s ${COMPFILE} ]]; then
271				rm ${COMPFILE} && continue
272			fi
273
274			_diff=$(diff -q ${TARGET} ${COMPFILE} 2>&1)
275			# files are the same: delete
276			[[ $? -eq 0 ]] && rm ${COMPFILE} && continue
277			# disable sdiff for binaries
278			echo "${_diff}" | head -1 | grep -q "Binary files" && \
279				IS_BIN=true
280		else
281			# missing files = binaries (to avoid comparison)
282			IS_BIN=true
283		fi
284
285		sm_diff_loop
286	done
287}
288
289sm_install() {
290	local _dmode _fgrp _fmode _fown
291	local _instdir=${TARGET%/*}
292	[[ -z ${_instdir} ]] && _instdir="/"
293
294	_dmode=$(stat -f "%OMp%OLp" .${_instdir}) || return
295	eval $(stat -f "_fmode=%OMp%OLp _fown=%Su _fgrp=%Sg" ${COMPFILE}) || return
296
297	if [[ ! -d ${_instdir} ]]; then
298		install -d -o root -g wheel -m ${_dmode} "${_instdir}" || return
299	fi
300
301	if ${IS_LINK}; then
302		_linkt=$(readlink ${COMPFILE})
303		(cd ${_instdir} && ln -sf ${_linkt} . && rm ${_TMPROOT}/${COMPFILE})
304		return
305	fi
306
307	if [[ -f ${TARGET} ]]; then
308		if typeset -f sm_rotate_bak >/dev/null; then
309			sm_rotate_bak || return
310		fi
311		mkdir -p ${_BKPDIR}/${_instdir} || return
312		cp -p ${TARGET} ${_BKPDIR}/${_instdir} || return
313	fi
314
315	if ! install -Fm ${_fmode} -o ${_fown} -g ${_fgrp} ${COMPFILE} ${_instdir}; then
316		rm ${_BKPDIR}/${COMPFILE} && return 1
317	fi
318	rm ${COMPFILE}
319
320	case ${TARGET} in
321	/etc/login.conf)
322		if [[ -f /etc/login.conf.db ]]; then
323			echo " (running cap_mkdb(1), needs a relog)"
324			sm_warn $(cap_mkdb /etc/login.conf 2>&1)
325		else
326			echo
327		fi
328		;;
329	/etc/mail/aliases)
330		if [[ -f /etc/mail/aliases.db ]]; then
331			echo " (running newaliases(8))"
332			sm_warn $(newaliases 2>&1 >/dev/null)
333		else
334			echo
335		fi
336		;;
337	*)
338		echo
339		;;
340	esac
341}
342
343sm_add_user_grp() {
344	local _name _c _d _e _f _G _g _L _pass _s _u
345	local _gr=./etc/group
346	local _pw=./etc/master.passwd
347
348	${PKGMODE} && return
349
350	while IFS=: read -r -- _name _pass _g _G; do
351		if ! getent group ${_name} >/dev/null; then
352			getent group ${_g} >/dev/null && \
353				sm_warn "Not adding group ${_name}, GID ${_g} already exists" && \
354				continue
355			echo "===> Adding the ${_name} group"
356			groupadd -g ${_g} ${_name}
357		fi
358	done <${_gr}
359
360	while IFS=: read -r -- _name _pass _u _g _L _f _e _c _d _s
361	do
362		if [[ ${_name} != root ]]; then
363			if ! getent passwd ${_name} >/dev/null; then
364				getent passwd ${_u} >/dev/null && \
365					sm_warn "Not adding user ${_name}, UID ${_u} already exists" && \
366					continue
367				echo "===> Adding the ${_name} user"
368				[[ -z ${_L} ]] || _L="-L ${_L}"
369				useradd -c "${_c}" -d ${_d} -e ${_e} -f ${_f} \
370					-g ${_g} ${_L} -s ${_s} -u ${_u} \
371					${_name} >/dev/null
372			fi
373		fi
374	done <${_pw}
375}
376
377sm_warn_valid() {
378	# done as a separate function to print a warning with the
379	# filename above output from the check command
380	local _res
381
382	_res=$(eval $* 2>&1)
383	if [[ $? -ne 0 || -n ${_res} ]]; then
384	       sm_warn "${_file} appears to be invalid"
385	       echo "${_res}"
386	fi
387}
388
389sm_check_validity() {
390	local _file=$1.merged
391	local _fail
392
393	case $1 in
394	./etc/ssh/sshd_config)
395		sm_warn_valid sshd -f ${_file} -t ;;
396	./etc/pf.conf)
397		sm_warn_valid pfctl -nf ${_file} ;;
398	./etc/login.conf)
399		sm_warn_valid "cap_mkdb -f ${_TMPROOT}/login.conf.check ${_file} || true"
400		rm -f ${_TMPROOT}/login.conf.check.db ;;
401	esac
402}
403
404sm_merge_loop() {
405	local _instmerged _tomerge
406	echo "===> Type h at the sdiff prompt (%) to get usage help\n"
407	_tomerge=true
408	while ${_tomerge}; do
409		cp -p ${COMPFILE} ${COMPFILE}.merged
410		sdiff -as -w $(tput -T ${TERM:-vt100} cols) -o ${COMPFILE}.merged \
411			${TARGET} ${COMPFILE}
412		_instmerged=v
413		while [[ ${_instmerged} == v ]]; do
414			echo
415			echo "  Use 'e' to edit the merged file"
416			echo "  Use 'i' to install the merged file"
417			echo "  Use 'n' to view a diff between the merged and new files"
418			echo "  Use 'o' to view a diff between the old and merged files"
419			echo "  Use 'r' to re-do the merge"
420			echo "  Use 'v' to view the merged file"
421			echo "  Use 'x' to delete the merged file and go back to previous menu"
422			echo "  Default is to leave the temporary file to deal with by hand"
423			echo
424			sm_check_validity ${COMPFILE}
425			echo -n "===> How should I deal with the merged file? [Leave it for later] "
426			read _instmerged
427			case ${_instmerged} in
428			[eE])
429				echo "editing merged file...\n"
430				${EDITOR} ${COMPFILE}.merged
431				_instmerged=v
432				;;
433			[iI])
434				mv ${COMPFILE}.merged ${COMPFILE}
435				echo -n "\n===> Merging ${TARGET}"
436				sm_install || \
437					(echo && sm_warn "problem merging ${TARGET}")
438				_tomerge=false
439				;;
440			[nN])
441				(
442					echo "comparison between merged and new files:\n"
443					diff -u ${COMPFILE}.merged ${COMPFILE}
444				) | ${PAGER}
445				_instmerged=v
446				;;
447			[oO])
448				(
449					echo "comparison between old and merged files:\n"
450					diff -u ${TARGET} ${COMPFILE}.merged
451				) | ${PAGER}
452				_instmerged=v
453				;;
454			[rR])
455				rm ${COMPFILE}.merged
456				;;
457			[vV])
458				${PAGER} ${COMPFILE}.merged
459				;;
460			[xX])
461				rm ${COMPFILE}.merged
462				return 1
463				;;
464			'')
465				_tomerge=false
466				;;
467			*)
468				echo "invalid choice: ${_instmerged}"
469				_instmerged=v
470				;;
471			esac
472		done
473	done
474}
475
476sm_diff_loop() {
477	local i _handle _nonexistent
478
479	${BATCHMODE} && _handle=todo || _handle=v
480
481	FORCE_UPG=false
482	_nonexistent=false
483	while [[ ${_handle} == @(v|todo) ]]; do
484		if [[ -f ${TARGET} && -f ${COMPFILE} ]] && ! ${IS_LINK}; then
485			if ! ${DIFFMODE}; then
486				# automatically install files if current != new
487				# and current = old
488				for i in ${AUTO_UPG[@]}; do \
489					[[ ${i} == ${COMPFILE} ]] && FORCE_UPG=true
490				done
491				# automatically install files which differ
492				# only by CVS Id or that are binaries
493				if [[ -z $(diff -q -I'[$]OpenBSD:.*$' ${TARGET} ${COMPFILE}) ]] || \
494					${FORCE_UPG} || ${IS_BIN}; then
495					echo -n "===> Updating ${TARGET}"
496					sm_install || \
497						(echo && sm_warn "problem updating ${TARGET}")
498					return
499				fi
500			fi
501			if [[ ${_handle} == v ]]; then
502				(
503					echo "\n========================================================================\n"
504					echo "===> Displaying differences between ${COMPFILE} and installed version:"
505					echo
506					diff -u ${TARGET} ${COMPFILE}
507				) | ${PAGER}
508				echo
509			fi
510		else
511			# file does not exist on the target system
512			if ${DIFFMODE}; then
513				_nonexistent=true
514				${BATCHMODE} || echo "\n===> Missing ${TARGET}\n"
515			elif ${IS_LINK}; then
516				echo "===> Linking ${TARGET}"
517				sm_install || \
518					sm_warn "problem creating ${TARGET} link"
519				return
520			else
521				echo -n "===> Installing ${TARGET}"
522				sm_install || \
523					(echo && sm_warn "problem installing ${TARGET}")
524				return
525			fi
526		fi
527
528		if ! ${BATCHMODE}; then
529			echo "  Use 'd' to delete the temporary ${COMPFILE}"
530			echo "  Use 'i' to install the temporary ${COMPFILE}"
531			if ! ${_nonexistent} && ! ${IS_BIN} && \
532				! ${IS_LINK}; then
533				echo "  Use 'm' to merge the temporary and installed versions"
534				echo "  Use 'v' to view the diff results again"
535			fi
536			echo
537			echo "  Default is to leave the temporary file to deal with by hand"
538			echo
539			echo -n "How should I deal with this? [Leave it for later] "
540			read _handle
541		else
542			unset _handle
543		fi
544
545		case ${_handle} in
546		[dD])
547			rm ${COMPFILE}
548			echo "\n===> Deleting ${COMPFILE}"
549			;;
550		[iI])
551			echo
552			if ${IS_LINK}; then
553				echo "===> Linking ${TARGET}"
554				sm_install || \
555					sm_warn "problem creating ${TARGET} link"
556			else
557				echo -n "===> Updating ${TARGET}"
558				sm_install || \
559					(echo && sm_warn "problem updating ${TARGET}")
560			fi
561			;;
562		[mM])
563			if ! ${_nonexistent} && ! ${IS_BIN} && ! ${IS_LINK}; then
564				sm_merge_loop || _handle=todo
565			else
566				echo "invalid choice: ${_handle}\n"
567				_handle=todo
568			fi
569			;;
570		[vV])
571			if ! ${_nonexistent} && ! ${IS_BIN} && ! ${IS_LINK}; then
572				_handle=v
573			else
574				echo "invalid choice: ${_handle}\n"
575				_handle=todo
576			fi
577			;;
578		'')
579			echo -n
580			;;
581		*)
582			echo "invalid choice: ${_handle}\n"
583			_handle=todo
584			continue
585			;;
586		esac
587	done
588}
589
590sm_post() {
591	local _f
592
593	cd ${_TMPROOT} && \
594		find . -type d -depth -empty -exec rmdir -p '{}' + 2>/dev/null
595	rmdir ${_TMPROOT} 2>/dev/null
596
597	if [[ -d ${_TMPROOT} ]]; then
598		for _f in $(find ${_TMPROOT} ! -type d ! -name \*.merged -size +0)
599		do
600			sm_info "${_f##*${_TMPROOT}} unhandled, re-run ${0##*/} to merge the new version"
601			! ${DIFFMODE} && [[ -f ${_f} ]] && \
602				sed -i "/$(sha256 -q ${_f})/d" /var/sysmerge/*sum
603		done
604	fi
605
606	mtree -qdef /etc/mtree/4.4BSD.dist -p / -U >/dev/null
607	[[ -f /var/sysmerge/xetc.tgz ]] && \
608		mtree -qdef /etc/mtree/BSD.x11.dist -p / -U >/dev/null
609}
610
611BATCHMODE=false
612DIFFMODE=false
613PKGMODE=false
614
615while getopts bdp arg; do
616	case ${arg} in
617	b)	BATCHMODE=true;;
618	d)	DIFFMODE=true;;
619	p)	PKGMODE=true;;
620	*)	usage;;
621	esac
622done
623shift $(( OPTIND -1 ))
624[[ $# -ne 0 ]] && usage
625
626[[ $(id -u) -ne 0 ]] && echo "${0##*/}: need root privileges" && exit 1
627
628# global constants
629_BKPDIR=/var/sysmerge/backups
630_RELINT=$(uname -r | tr -d '.') || exit 1
631_TMPROOT=$(mktemp -d -p ${TMPDIR:-/tmp} sysmerge.XXXXXXXXXX) || exit 1
632readonly _BKPDIR _RELINT _TMPROOT
633
634[[ -z ${VISUAL} ]] && EDITOR=${EDITOR:-/usr/bin/vi} || EDITOR=${VISUAL}
635PAGER=${PAGER:-/usr/bin/more}
636
637mkdir -p ${_TMPROOT} || sm_error "cannot create ${_TMPROOT}"
638cd ${_TMPROOT} || sm_error "cannot enter ${_TMPROOT}"
639
640sm_run && sm_post
641