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