xref: /openbsd/usr.sbin/sysmerge/sysmerge.sh (revision 6f40fd34)
1#!/bin/ksh -
2#
3# $OpenBSD: sysmerge.sh,v 1.230 2017/03/06 08:16:24 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	# XXX remove after OPENBSD_6_1
183	rm -f /var/sysmerge/examplessum
184
185	sm_extract_sets
186	sm_add_user_grp
187	sm_cp_pkg_samples
188
189	for _i in etcsum xetcsum pkgsum; do
190		if [[ -f /var/sysmerge/${_i} && \
191			-f ./var/sysmerge/${_i} ]] && \
192			! ${DIFFMODE}; then
193			# redirect stderr: file may not exist
194			_matchsum=$(sha256 -c /var/sysmerge/${_i} 2>/dev/null | \
195				sed -n 's/^(SHA256) \(.*\): OK$/\1/p')
196			# delete file in temproot if it has not changed since
197			# last release and is present in current installation
198			for _j in ${_matchsum}; do
199				# skip sum files
200				[[ ${_j} == ./var/sysmerge/${_i} ]] && continue
201				[[ -f ${_j#.} && -f ${_j} ]] && \
202					rm ${_j}
203			done
204
205			# set auto-upgradable files
206			_mismatch=$(diff -u ./var/sysmerge/${_i} /var/sysmerge/${_i} | \
207				sed -n 's/^+SHA256 (\(.*\)).*/\1/p')
208			for _k in ${_mismatch}; do
209				# skip sum files
210				[[ ${_k} == ./var/sysmerge/${_i} ]] && continue
211				# compare CVS Id first so if the file hasn't been modified,
212				# it will be deleted from temproot and ignored from comparison;
213				# several files are generated from scripts so CVS ID is not a
214				# reliable way of detecting changes: leave for a full diff
215				if ! ${PKGMODE} && \
216					[[ ${_k} != ./etc/@(fbtab|ttys) && \
217					! -h ${_k} ]]; then
218					_cvsid1=$(sed -n "/[$]OpenBSD:.*Exp [$]/{p;q;}" ${_k#.} 2>/dev/null)
219					_cvsid2=$(sed -n "/[$]OpenBSD:.*Exp [$]/{p;q;}" ${_k} 2>/dev/null)
220					[[ -n ${_cvsid1} ]] && \
221						[[ ${_cvsid1} == ${_cvsid2} ]] && \
222						[[ -f ${_k} ]] && rm ${_k} && \
223						continue
224				fi
225				# redirect stderr: file may not exist
226				_cursum=$(cd / && sha256 ${_k} 2>/dev/null)
227				grep -q "${_cursum}" /var/sysmerge/${_i} && \
228					! grep -q "${_cursum}" ./var/sysmerge/${_i} && \
229					_auto_upg="${_auto_upg} ${_k}"
230			done
231			[[ -n ${_auto_upg} ]] && set -A AUTO_UPG -- ${_auto_upg}
232		fi
233		[[ -f ./var/sysmerge/${_i} ]] && \
234			mv ./var/sysmerge/${_i} /var/sysmerge/${_i}
235	done
236
237	# files we don't want/need to deal with
238	_ignorefiles="/etc/group
239		      /etc/localtime
240		      /etc/master.passwd
241		      /etc/motd
242		      /etc/passwd
243		      /etc/pwd.db
244		      /etc/spwd.db
245		      /var/db/locate.database
246		      /var/mail/root"
247	# in case X(7) is not installed, xetcsum is not removed by the loop above
248	_ignorefiles="${_ignorefiles} /var/sysmerge/xetcsum"
249	[[ -f /etc/sysmerge.ignore ]] && \
250		_ignorefiles="${_ignorefiles} $(stripcom /etc/sysmerge.ignore)"
251	for _i in ${_ignorefiles}; do
252		rm -f ./${_i}
253	done
254
255	# aliases(5) needs to be handled last in case mailer.conf(5) changes
256	_c1=$(find . -type f -or -type l | grep -v '^./etc/mail/aliases$')
257	[[ -f ./etc/mail/aliases ]] && _c2="./etc/mail/aliases"
258	for COMPFILE in ${_c1} ${_c2}; do
259		IS_BIN=false
260		IS_LINK=false
261		TARGET=${COMPFILE#.}
262
263		# links need to be treated in a different way
264		if [[ -h ${COMPFILE} ]]; then
265			IS_LINK=true
266			[[ -h ${TARGET} && \
267				$(readlink ${COMPFILE}) == $(readlink ${TARGET}) ]] && \
268				rm ${COMPFILE} && continue
269		elif [[ -f ${TARGET} ]]; then
270			# empty files = binaries (to avoid comparison);
271			# only process them if they don't exist on the system
272			if [[ ! -s ${COMPFILE} ]]; then
273				rm ${COMPFILE} && continue
274			fi
275
276			_diff=$(diff -q ${TARGET} ${COMPFILE} 2>&1)
277			# files are the same: delete
278			[[ $? -eq 0 ]] && rm ${COMPFILE} && continue
279			# disable sdiff for binaries
280			echo "${_diff}" | head -1 | grep -q "Binary files" && \
281				IS_BIN=true
282		else
283			# missing files = binaries (to avoid comparison)
284			IS_BIN=true
285		fi
286
287		sm_diff_loop
288	done
289}
290
291sm_install() {
292	local _dmode _fgrp _fmode _fown
293	local _instdir=${TARGET%/*}
294	[[ -z ${_instdir} ]] && _instdir="/"
295
296	_dmode=$(stat -f "%OMp%OLp" .${_instdir}) || return
297	eval $(stat -f "_fmode=%OMp%OLp _fown=%Su _fgrp=%Sg" ${COMPFILE}) || return
298
299	if [[ ! -d ${_instdir} ]]; then
300		install -d -o root -g wheel -m ${_dmode} "${_instdir}" || return
301	fi
302
303	if ${IS_LINK}; then
304		_linkt=$(readlink ${COMPFILE})
305		(cd ${_instdir} && ln -sf ${_linkt} . && rm ${_TMPROOT}/${COMPFILE})
306		return
307	fi
308
309	if [[ -f ${TARGET} ]]; then
310		if typeset -f sm_rotate_bak >/dev/null; then
311			sm_rotate_bak || return
312		fi
313		mkdir -p ${_BKPDIR}/${_instdir} || return
314		cp -p ${TARGET} ${_BKPDIR}/${_instdir} || return
315	fi
316
317	if ! install -m ${_fmode} -o ${_fown} -g ${_fgrp} ${COMPFILE} ${_instdir}; then
318		rm ${_BKPDIR}/${COMPFILE} && return 1
319	fi
320	rm ${COMPFILE}
321
322	case ${TARGET} in
323	/etc/login.conf)
324		if [[ -f /etc/login.conf.db ]]; then
325			echo " (running cap_mkdb(1), needs a relog)"
326			sm_warn $(cap_mkdb /etc/login.conf 2>&1)
327		else
328			echo
329		fi
330		;;
331	/etc/mail/aliases)
332		if [[ -f /etc/mail/aliases.db ]]; then
333			echo " (running newaliases(8))"
334			sm_warn $(newaliases 2>&1 >/dev/null)
335		else
336			echo
337		fi
338		;;
339	*)
340		echo
341		;;
342	esac
343}
344
345sm_add_user_grp() {
346	local _g _p _gid _u _rest
347	local _gr=./etc/group
348	local _pw=./etc/master.passwd
349
350	${PKGMODE} && return
351
352	while IFS=: read -r -- _g _p _gid _rest; do
353		if ! grep -Eq "^${_g}:" /etc/group; then
354			getent group ${_gid} >/dev/null && \
355				sm_warn "Not adding group ${_g}, GID ${_gid} already exists" && \
356				continue
357			echo "===> Adding the ${_g} group"
358			groupadd -g ${_gid} ${_g}
359		fi
360	done <${_gr}
361
362	while IFS=: read -r -- _u _p _uid _rest; do
363		if [[ ${_u} != root ]]; then
364			if ! grep -Eq "^${_u}:" /etc/master.passwd; then
365				getent passwd ${_uid} >/dev/null && \
366					sm_warn "Not adding user ${_u}, UID ${_uid} already exists" && \
367					continue
368				echo "===> Adding the ${_u} user"
369				chpass -a "${_u}:${_p}:${_uid}:${_rest}"
370			fi
371		fi
372	done <${_pw}
373}
374
375sm_warn_valid() {
376	# done as a separate function to print a warning with the
377	# filename above output from the check command
378	local _res
379
380	_res=$(eval $* 2>&1)
381	if [[ $? -ne 0 || -n ${_res} ]]; then
382	       sm_warn "${_file} appears to be invalid"
383	       echo "${_res}"
384	fi
385}
386
387sm_check_validity() {
388	local _file=$1.merged
389	local _fail
390
391	case $1 in
392	./etc/ssh/sshd_config)
393		sm_warn_valid sshd -f ${_file} -t ;;
394	./etc/pf.conf)
395		sm_warn_valid pfctl -nf ${_file} ;;
396	./etc/login.conf)
397		sm_warn_valid "cap_mkdb -f ${_TMPROOT}/login.conf.check ${_file} || true"
398		rm -f ${_TMPROOT}/login.conf.check.db ;;
399	esac
400}
401
402sm_merge_loop() {
403	local _instmerged _tomerge
404	echo "===> Type h at the sdiff prompt (%) to get usage help\n"
405	_tomerge=true
406	while ${_tomerge}; do
407		cp -p ${COMPFILE} ${COMPFILE}.merged
408		sdiff -as -w $(tput -T ${TERM:=vt100} cols) -o ${COMPFILE}.merged \
409			${TARGET} ${COMPFILE}
410		_instmerged=v
411		while [[ ${_instmerged} == v ]]; do
412			echo
413			echo "  Use 'e' to edit the merged file"
414			echo "  Use 'i' to install the merged file"
415			echo "  Use 'n' to view a diff between the merged and new files"
416			echo "  Use 'o' to view a diff between the old and merged files"
417			echo "  Use 'r' to re-do the merge"
418			echo "  Use 'v' to view the merged file"
419			echo "  Use 'x' to delete the merged file and go back to previous menu"
420			echo "  Default is to leave the temporary file to deal with by hand"
421			echo
422			sm_check_validity ${COMPFILE}
423			echo -n "===> How should I deal with the merged file? [Leave it for later] "
424			read _instmerged
425			case ${_instmerged} in
426			[eE])
427				echo "editing merged file...\n"
428				${EDITOR} ${COMPFILE}.merged
429				_instmerged=v
430				;;
431			[iI])
432				mv ${COMPFILE}.merged ${COMPFILE}
433				echo -n "\n===> Merging ${TARGET}"
434				sm_install || \
435					(echo && sm_warn "problem merging ${TARGET}")
436				_tomerge=false
437				;;
438			[nN])
439				(
440					echo "comparison between merged and new files:\n"
441					diff -u ${COMPFILE}.merged ${COMPFILE}
442				) | ${PAGER}
443				_instmerged=v
444				;;
445			[oO])
446				(
447					echo "comparison between old and merged files:\n"
448					diff -u ${TARGET} ${COMPFILE}.merged
449				) | ${PAGER}
450				_instmerged=v
451				;;
452			[rR])
453				rm ${COMPFILE}.merged
454				;;
455			[vV])
456				${PAGER} ${COMPFILE}.merged
457				;;
458			[xX])
459				rm ${COMPFILE}.merged
460				return 1
461				;;
462			'')
463				_tomerge=false
464				;;
465			*)
466				echo "invalid choice: ${_instmerged}"
467				_instmerged=v
468				;;
469			esac
470		done
471	done
472}
473
474sm_diff_loop() {
475	local i _handle _nonexistent
476
477	${BATCHMODE} && _handle=todo || _handle=v
478
479	FORCE_UPG=false
480	_nonexistent=false
481	while [[ ${_handle} == @(v|todo) ]]; do
482		if [[ -f ${TARGET} && -f ${COMPFILE} ]] && ! ${IS_LINK}; then
483			if ! ${DIFFMODE}; then
484				# automatically install files if current != new
485				# and current = old
486				for i in ${AUTO_UPG[@]}; do \
487					[[ ${i} == ${COMPFILE} ]] && FORCE_UPG=true
488				done
489				# automatically install files which differ
490				# only by CVS Id or that are binaries
491				if [[ -z $(diff -q -I'[$]OpenBSD:.*$' ${TARGET} ${COMPFILE}) ]] || \
492					${FORCE_UPG} || ${IS_BIN}; then
493					echo -n "===> Updating ${TARGET}"
494					sm_install || \
495						(echo && sm_warn "problem updating ${TARGET}")
496					return
497				fi
498			fi
499			if [[ ${_handle} == v ]]; then
500				(
501					echo "\n========================================================================\n"
502					echo "===> Displaying differences between ${COMPFILE} and installed version:"
503					echo
504					diff -u ${TARGET} ${COMPFILE}
505				) | ${PAGER}
506				echo
507			fi
508		else
509			# file does not exist on the target system
510			if ${DIFFMODE}; then
511				_nonexistent=true
512				${BATCHMODE} || echo "\n===> Missing ${TARGET}\n"
513			elif ${IS_LINK}; then
514				echo "===> Linking ${TARGET}"
515				sm_install || \
516					sm_warn "problem creating ${TARGET} link"
517				return
518			else
519				echo -n "===> Installing ${TARGET}"
520				sm_install || \
521					(echo && sm_warn "problem installing ${TARGET}")
522				return
523			fi
524		fi
525
526		if ! ${BATCHMODE}; then
527			echo "  Use 'd' to delete the temporary ${COMPFILE}"
528			echo "  Use 'i' to install the temporary ${COMPFILE}"
529			if ! ${_nonexistent} && ! ${IS_BIN} && \
530				! ${IS_LINK}; then
531				echo "  Use 'm' to merge the temporary and installed versions"
532				echo "  Use 'v' to view the diff results again"
533			fi
534			echo
535			echo "  Default is to leave the temporary file to deal with by hand"
536			echo
537			echo -n "How should I deal with this? [Leave it for later] "
538			read _handle
539		else
540			unset _handle
541		fi
542
543		case ${_handle} in
544		[dD])
545			rm ${COMPFILE}
546			echo "\n===> Deleting ${COMPFILE}"
547			;;
548		[iI])
549			echo
550			if ${IS_LINK}; then
551				echo "===> Linking ${TARGET}"
552				sm_install || \
553					sm_warn "problem creating ${TARGET} link"
554			else
555				echo -n "===> Updating ${TARGET}"
556				sm_install || \
557					(echo && sm_warn "problem updating ${TARGET}")
558			fi
559			;;
560		[mM])
561			if ! ${_nonexistent} && ! ${IS_BIN} && ! ${IS_LINK}; then
562				sm_merge_loop || _handle=todo
563			else
564				echo "invalid choice: ${_handle}\n"
565				_handle=todo
566			fi
567			;;
568		[vV])
569			if ! ${_nonexistent} && ! ${IS_BIN} && ! ${IS_LINK}; then
570				_handle=v
571			else
572				echo "invalid choice: ${_handle}\n"
573				_handle=todo
574			fi
575			;;
576		'')
577			echo -n
578			;;
579		*)
580			echo "invalid choice: ${_handle}\n"
581			_handle=todo
582			continue
583			;;
584		esac
585	done
586}
587
588sm_post() {
589	local _f
590
591	cd ${_TMPROOT} && \
592		find . -type d -depth -empty -exec rmdir -p '{}' + 2>/dev/null
593	rmdir ${_TMPROOT} 2>/dev/null
594
595	if [[ -d ${_TMPROOT} ]]; then
596		for _f in $(find ${_TMPROOT} ! -type d ! -name \*.merged -size +0)
597		do
598			sm_info "${_f##*${_TMPROOT}} unhandled, re-run ${0##*/} to merge the new version"
599			! ${DIFFMODE} && [[ -f ${_f} ]] && \
600				sed -i "/$(sha256 -q ${_f})/d" /var/sysmerge/*sum
601		done
602	fi
603
604	mtree -qdef /etc/mtree/4.4BSD.dist -p / -U >/dev/null
605	[[ -f /var/sysmerge/xetc.tgz ]] && \
606		mtree -qdef /etc/mtree/BSD.x11.dist -p / -U >/dev/null
607}
608
609BATCHMODE=false
610DIFFMODE=false
611PKGMODE=false
612
613while getopts bdp arg; do
614	case ${arg} in
615	b)	BATCHMODE=true;;
616	d)	DIFFMODE=true;;
617	p)	PKGMODE=true;;
618	*)	usage;;
619	esac
620done
621shift $(( OPTIND -1 ))
622[[ $# -ne 0 ]] && usage
623
624[[ $(id -u) -ne 0 ]] && echo "${0##*/}: need root privileges" && exit 1
625
626# global constants
627_BKPDIR=/var/sysmerge/backups
628_RELINT=$(uname -r | tr -d '.') || exit 1
629_TMPROOT=$(mktemp -d -p /tmp sysmerge.XXXXXXXXXX) || exit 1
630readonly _BKPDIR _RELINT _TMPROOT
631
632[[ -z ${VISUAL} ]] && EDITOR=${EDITOR:=/usr/bin/vi} || EDITOR=${VISUAL}
633PAGER=${PAGER:=/usr/bin/more}
634
635mkdir -p ${_TMPROOT} || sm_error "cannot create ${_TMPROOT}"
636cd ${_TMPROOT} || sm_error "cannot enter ${_TMPROOT}"
637
638sm_run && sm_post
639