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