xref: /openbsd-src/usr.sbin/sysmerge/sysmerge.sh (revision f2da64fbbbf1b03f09f390ab01267c93dfd77c4c)
1#!/bin/ksh -
2#
3# $OpenBSD: sysmerge.sh,v 1.227 2016/07/30 06:31:17 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 _l _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			echo "===> Adding the ${_g} group"
355			groupadd -g ${_gid} ${_g}
356		fi
357	done <${_gr}
358
359	while read _l; do
360		_u=${_l%%:*}
361		if [[ ${_u} != root ]]; then
362			if ! grep -Eq "^${_u}:" /etc/master.passwd; then
363				echo "===> Adding the ${_u} user"
364				chpass -a "${_l}"
365			fi
366		fi
367	done <${_pw}
368}
369
370sm_warn_valid() {
371	# done as a separate function to print a warning with the
372	# filename above output from the check command
373	local _res
374
375	_res=$(eval $* 2>&1)
376	if [[ $? -ne 0 || -n ${_res} ]]; then
377	       sm_warn "${_file} appears to be invalid"
378	       echo "${_res}"
379	fi
380}
381
382sm_check_validity() {
383	local _file=$1.merged
384	local _fail
385
386	case $1 in
387	./etc/ssh/sshd_config)
388		sm_warn_valid sshd -f ${_file} -t ;;
389	./etc/pf.conf)
390		sm_warn_valid pfctl -nf ${_file} ;;
391	./etc/login.conf)
392		sm_warn_valid "cap_mkdb -f ${_TMPROOT}/login.conf.check ${_file} || true"
393		rm -f ${_TMPROOT}/login.conf.check.db ;;
394	esac
395}
396
397sm_merge_loop() {
398	local _instmerged _tomerge
399	echo "===> Type h at the sdiff prompt (%) to get usage help\n"
400	_tomerge=true
401	while ${_tomerge}; do
402		cp -p ${COMPFILE} ${COMPFILE}.merged
403		sdiff -as -w $(tput -T ${TERM:=vt100} cols) -o ${COMPFILE}.merged \
404			${TARGET} ${COMPFILE}
405		_instmerged=v
406		while [[ ${_instmerged} == v ]]; do
407			echo
408			echo "  Use 'e' to edit the merged file"
409			echo "  Use 'i' to install the merged file"
410			echo "  Use 'n' to view a diff between the merged and new files"
411			echo "  Use 'o' to view a diff between the old and merged files"
412			echo "  Use 'r' to re-do the merge"
413			echo "  Use 'v' to view the merged file"
414			echo "  Use 'x' to delete the merged file and go back to previous menu"
415			echo "  Default is to leave the temporary file to deal with by hand"
416			echo
417			sm_check_validity ${COMPFILE}
418			echo -n "===> How should I deal with the merged file? [Leave it for later] "
419			read _instmerged
420			case ${_instmerged} in
421			[eE])
422				echo "editing merged file...\n"
423				${EDITOR} ${COMPFILE}.merged
424				_instmerged=v
425				;;
426			[iI])
427				mv ${COMPFILE}.merged ${COMPFILE}
428				echo -n "\n===> Merging ${TARGET}"
429				sm_install || \
430					(echo && sm_warn "problem merging ${TARGET}")
431				_tomerge=false
432				;;
433			[nN])
434				(
435					echo "comparison between merged and new files:\n"
436					diff -u ${COMPFILE}.merged ${COMPFILE}
437				) | ${PAGER}
438				_instmerged=v
439				;;
440			[oO])
441				(
442					echo "comparison between old and merged files:\n"
443					diff -u ${TARGET} ${COMPFILE}.merged
444				) | ${PAGER}
445				_instmerged=v
446				;;
447			[rR])
448				rm ${COMPFILE}.merged
449				;;
450			[vV])
451				${PAGER} ${COMPFILE}.merged
452				;;
453			[xX])
454				rm ${COMPFILE}.merged
455				return 1
456				;;
457			'')
458				_tomerge=false
459				;;
460			*)
461				echo "invalid choice: ${_instmerged}"
462				_instmerged=v
463				;;
464			esac
465		done
466	done
467}
468
469sm_diff_loop() {
470	local i _handle _nonexistent
471
472	${BATCHMODE} && _handle=todo || _handle=v
473
474	FORCE_UPG=false
475	_nonexistent=false
476	while [[ ${_handle} == @(v|todo) ]]; do
477		if [[ -f ${TARGET} && -f ${COMPFILE} ]] && ! ${IS_LINK}; then
478			if ! ${DIFFMODE}; then
479				# automatically install files if current != new
480				# and current = old
481				for i in ${AUTO_UPG[@]}; do \
482					[[ ${i} == ${COMPFILE} ]] && FORCE_UPG=true
483				done
484				# automatically install files which differ
485				# only by CVS Id or that are binaries
486				if [[ -z $(diff -q -I'[$]OpenBSD:.*$' ${TARGET} ${COMPFILE}) ]] || \
487					${FORCE_UPG} || ${IS_BIN}; then
488					echo -n "===> Updating ${TARGET}"
489					sm_install || \
490						(echo && sm_warn "problem updating ${TARGET}")
491					return
492				fi
493			fi
494			if [[ ${_handle} == v ]]; then
495				(
496					echo "\n========================================================================\n"
497					echo "===> Displaying differences between ${COMPFILE} and installed version:"
498					echo
499					diff -u ${TARGET} ${COMPFILE}
500				) | ${PAGER}
501				echo
502			fi
503		else
504			# file does not exist on the target system
505			if ${DIFFMODE}; then
506				_nonexistent=true
507				${BATCHMODE} || echo "\n===> Missing ${TARGET}\n"
508			elif ${IS_LINK}; then
509				echo "===> Linking ${TARGET}"
510				sm_install || \
511					sm_warn "problem creating ${TARGET} link"
512				return
513			else
514				echo -n "===> Installing ${TARGET}"
515				sm_install || \
516					(echo && sm_warn "problem installing ${TARGET}")
517				return
518			fi
519		fi
520
521		if ! ${BATCHMODE}; then
522			echo "  Use 'd' to delete the temporary ${COMPFILE}"
523			echo "  Use 'i' to install the temporary ${COMPFILE}"
524			if ! ${_nonexistent} && ! ${IS_BIN} && \
525				! ${IS_LINK}; then
526				echo "  Use 'm' to merge the temporary and installed versions"
527				echo "  Use 'v' to view the diff results again"
528			fi
529			echo
530			echo "  Default is to leave the temporary file to deal with by hand"
531			echo
532			echo -n "How should I deal with this? [Leave it for later] "
533			read _handle
534		else
535			unset _handle
536		fi
537
538		case ${_handle} in
539		[dD])
540			rm ${COMPFILE}
541			echo "\n===> Deleting ${COMPFILE}"
542			;;
543		[iI])
544			echo
545			if ${IS_LINK}; then
546				echo "===> Linking ${TARGET}"
547				sm_install || \
548					sm_warn "problem creating ${TARGET} link"
549			else
550				echo -n "===> Updating ${TARGET}"
551				sm_install || \
552					(echo && sm_warn "problem updating ${TARGET}")
553			fi
554			;;
555		[mM])
556			if ! ${_nonexistent} && ! ${IS_BIN} && ! ${IS_LINK}; then
557				sm_merge_loop || _handle=todo
558			else
559				echo "invalid choice: ${_handle}\n"
560				_handle=todo
561			fi
562			;;
563		[vV])
564			if ! ${_nonexistent} && ! ${IS_BIN} && ! ${IS_LINK}; then
565				_handle=v
566			else
567				echo "invalid choice: ${_handle}\n"
568				_handle=todo
569			fi
570			;;
571		'')
572			echo -n
573			;;
574		*)
575			echo "invalid choice: ${_handle}\n"
576			_handle=todo
577			continue
578			;;
579		esac
580	done
581}
582
583sm_post() {
584	local _f
585
586	cd ${_TMPROOT} && \
587		find . -type d -depth -empty -exec rmdir -p '{}' + 2>/dev/null
588	rmdir ${_TMPROOT} 2>/dev/null
589
590	if [[ -d ${_TMPROOT} ]]; then
591		for _f in $(find ${_TMPROOT} ! -type d ! -name \*.merged -size +0)
592		do
593			sm_info "${_f##*${_TMPROOT}} unhandled, re-run ${0##*/} to merge the new version"
594			! ${DIFFMODE} && [[ -f ${_f} ]] && \
595				sed -i "/$(sha256 -q ${_f})/d" /var/sysmerge/*sum
596		done
597	fi
598
599	mtree -qdef /etc/mtree/4.4BSD.dist -p / -U >/dev/null
600	[[ -d /etc/X11 ]] && \
601		mtree -qdef /etc/mtree/BSD.x11.dist -p / -U >/dev/null
602}
603
604BATCHMODE=false
605DIFFMODE=false
606PKGMODE=false
607
608while getopts bdp arg; do
609	case ${arg} in
610	b)	BATCHMODE=true;;
611	d)	DIFFMODE=true;;
612	p)	PKGMODE=true;;
613	*)	usage;;
614	esac
615done
616shift $(( OPTIND -1 ))
617[[ $# -ne 0 ]] && usage
618
619[[ $(id -u) -ne 0 ]] && echo "${0##*/}: need root privileges" && usage
620
621# global constants
622_BKPDIR=/var/sysmerge/backups
623_RELINT=$(uname -r | tr -d '.') || exit 1
624_TMPROOT=$(mktemp -d -p /tmp sysmerge.XXXXXXXXXX) || exit 1
625readonly _BKPDIR _RELINT _TMPROOT
626
627[[ -z ${VISUAL} ]] && EDITOR=${EDITOR:=/usr/bin/vi} || EDITOR=${VISUAL}
628PAGER=${PAGER:=/usr/bin/more}
629
630mkdir -p ${_TMPROOT} || sm_error "cannot create ${_TMPROOT}"
631cd ${_TMPROOT} || sm_error "cannot enter ${_TMPROOT}"
632
633sm_run && sm_post
634