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