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