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