diff options
Diffstat (limited to 'usr.sbin/etcupdate')
| -rw-r--r-- | usr.sbin/etcupdate/Makefile | 9 | ||||
| -rw-r--r-- | usr.sbin/etcupdate/Makefile.depend | 10 | ||||
| -rw-r--r-- | usr.sbin/etcupdate/etcupdate.8 | 933 | ||||
| -rwxr-xr-x | usr.sbin/etcupdate/etcupdate.sh | 1963 | ||||
| -rw-r--r-- | usr.sbin/etcupdate/tests/Makefile | 13 | ||||
| -rw-r--r-- | usr.sbin/etcupdate/tests/Makefile.depend | 10 | ||||
| -rw-r--r-- | usr.sbin/etcupdate/tests/always_test.sh | 629 | ||||
| -rw-r--r-- | usr.sbin/etcupdate/tests/conflicts_test.sh | 293 | ||||
| -rw-r--r-- | usr.sbin/etcupdate/tests/fbsdid_test.sh | 393 | ||||
| -rw-r--r-- | usr.sbin/etcupdate/tests/ignore_test.sh | 275 | ||||
| -rw-r--r-- | usr.sbin/etcupdate/tests/preworld_test.sh | 251 | ||||
| -rw-r--r-- | usr.sbin/etcupdate/tests/tests_test.sh | 1020 | ||||
| -rw-r--r-- | usr.sbin/etcupdate/tests/tzsetup_test.sh | 239 |
13 files changed, 6038 insertions, 0 deletions
diff --git a/usr.sbin/etcupdate/Makefile b/usr.sbin/etcupdate/Makefile new file mode 100644 index 000000000000..df1fcbeaf128 --- /dev/null +++ b/usr.sbin/etcupdate/Makefile @@ -0,0 +1,9 @@ +.include <src.opts.mk> + +SCRIPTS=etcupdate.sh +MAN= etcupdate.8 + +HAS_TESTS= +SUBDIR.${MK_TESTS}+= tests + +.include <bsd.prog.mk> diff --git a/usr.sbin/etcupdate/Makefile.depend b/usr.sbin/etcupdate/Makefile.depend new file mode 100644 index 000000000000..11aba52f82cf --- /dev/null +++ b/usr.sbin/etcupdate/Makefile.depend @@ -0,0 +1,10 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + + +.include <dirdeps.mk> + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/usr.sbin/etcupdate/etcupdate.8 b/usr.sbin/etcupdate/etcupdate.8 new file mode 100644 index 000000000000..bf7ddb792907 --- /dev/null +++ b/usr.sbin/etcupdate/etcupdate.8 @@ -0,0 +1,933 @@ +.\" Copyright (c) 2010-2013 Hudson River Trading LLC +.\" Written by: John H. Baldwin <jhb@FreeBSD.org> +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd July 15, 2024 +.Dt ETCUPDATE 8 +.Os +.Sh NAME +.Nm etcupdate +.Nd "manage updates to system files not updated by installworld" +.Sh SYNOPSIS +.Nm +.Op Fl npBFN +.Op Fl d Ar workdir +.Op Fl r | Fl s Ar source | Fl t Ar tarball +.Op Fl A Ar patterns +.Op Fl D Ar destdir +.Op Fl I Ar patterns +.Op Fl L Ar logfile +.Op Fl M Ar options +.Op Fl m Ar make +.Nm +.Cm build +.Op Fl BN +.Op Fl d Ar workdir +.Op Fl s Ar source +.Op Fl L Ar logfile +.Op Fl M Ar options +.Op Fl m Ar make +.Ar tarball +.Nm +.Cm diff +.Op Fl d Ar workdir +.Op Fl D Ar destdir +.Op Fl I Ar patterns +.Op Fl L Ar logfile +.Nm +.Cm extract +.Op Fl BN +.Op Fl d Ar workdir +.Op Fl s Ar source | Fl t Ar tarball +.Op Fl D Ar destdir +.Op Fl L Ar logfile +.Op Fl M Ar options +.Op Fl m Ar make +.Nm +.Cm resolve +.Op Fl p +.Op Fl d Ar workdir +.Op Fl D Ar destdir +.Op Fl L Ar logfile +.Nm +.Cm revert +.Op Fl d Ar workdir +.Op Fl D Ar destdir +.Op Fl L Ar logfile +.Ar +.Nm +.Cm status +.Op Fl d Ar workdir +.Op Fl D Ar destdir +.Sh DESCRIPTION +The +.Nm +utility is a tool for managing updates to files that are not updated as +part of +.Sq make installworld +such as files in +.Pa /etc . +It manages updates by doing a three-way merge of changes made to these +files against the local versions. +It is also designed to minimize the amount of user intervention with +the goal of simplifying upgrades for clusters of machines. +.Pp +To perform a three-way merge, +.Nm +keeps copies of the current and previous versions of files that it manages. +These copies are stored in two trees known as the +.Dq current +and +.Dq previous +trees. +During a merge, +.Nm +compares the +.Dq current +and +.Dq previous +copies of each file to determine which changes need to be merged into the +local version of each file. +If a file can be updated without generating a conflict, +.Nm +will update the file automatically. +If the local changes to a file conflict with the changes made to a file in +the source tree, +then a merge conflict is generated. +The conflict must be resolved after the merge has finished. +The +.Nm +utility will not perform a new merge until all conflicts from an earlier +merge are resolved. +.Sh MODES +The +.Nm +utility supports several modes of operation. +The mode is specified via an optional command argument. +If present, the command must be the first argument on the command line. +If a command is not specified, the default mode is used. +.Ss Default Mode +The default mode merges changes from the source tree to the destination +directory. +First, +it updates the +.Dq current +and +.Dq previous +trees. +Next, +it compares the two trees merging changes into the destination directory. +Finally, +it displays warnings for any conditions it could not handle automatically. +.Pp +If the +.Fl r +option is not specified, +then the first step taken is to update the +.Dq current +and +.Dq previous +trees. +If a +.Dq current +tree already exists, +then that tree is saved as the +.Dq previous +tree. +An older +.Dq previous +tree is removed if it exists. +By default the new +.Dq current +tree is built from a source tree. +However, +if a tarball is specified via the +.Fl t +option, +then the tree is extracted from that tarball instead. +.Pp +Next, +.Nm +compares the files in the +.Dq current +and +.Dq previous +trees. +If a file was removed from the +.Dq current +tree, +then it will be removed from the destination directory only if it +does not have any local modifications. +If a file was added to the +.Dq current +tree, +then it will be copied to the destination directory only if it +would not clobber an existing file. +If a file is changed in the +.Dq current +tree, +then +.Nm +will attempt to merge the changes into the version of the file in the +destination directory. +If the merge encounters conflicts, +then a version of the file with conflict markers will be saved for +future resolution. +If the merge does not encounter conflicts, +then the merged version of the file will be saved in the destination +directory. +If +.Nm +is not able to safely merge in changes to a file other than a merge conflict, +it will generate a warning. +.Pp +For each file that is updated a line will be output with a leading character +to indicate the action taken. +The possible actions follow: +.Pp +.Bl -tag -width "A" -compact -offset indent +.It A +Added +.It C +Conflict +.It D +Deleted +.It M +Merged +.It U +Updated +.El +.Pp +Finally, +if any warnings were encountered they are displayed after the merge has +completed. +.Pp +Note that for certain files +.Nm +will perform post-install actions any time that the file is updated. +Specifically, +.Xr pwd_mkdb 8 +is invoked if +.Pa /etc/master.passwd +is changed, +.Xr cap_mkdb 1 +is invoked to update +.Pa /etc/login.conf.db +if +.Pa /etc/login.conf +is changed, +.Xr newaliases 1 +is invoked if +.Pa /etc/mail/aliases +is changed, +.Xr services_mkdb 8 +is invoked if +.Pa /etc/services +is changed, +.Xr tzsetup 8 +is invoked if +.Pa /etc/localtime +is changed and if +.Fa /var/db/zoneinfo +exists, +and +.Pa /etc/rc.d/motd +is invoked if +.Pa /etc/motd +is changed. +One exception is that if +.Pa /etc/mail/aliases +is changed and the destination directory is not the default, +then a warning will be issued instead. +This is due to a limitation of the +.Xr newaliases 1 +command. +Similarly, +if +.Pa /etc/motd +is changed and the destination directory is not the default, +then +.Pa /etc/rc.d/motd +will not be executed due to a limitation of that script. +In this case no warning is issued as the result of +.Pa /etc/rc.d/motd +is merely cosmetic and will be corrected on the next reboot. +.Ss Build Mode +The +.Cm build +mode is used to build a tarball that contains a snapshot of a +.Dq current +tree. +This tarball can be used by the default and extract modes. +Using a tarball can allow +.Nm +to perform a merge without requiring a source tree that matches the +currently installed world. +The +.Fa tarball +argument specifies the name of the file to create. +The file will be a +.Xr tar 5 +file compressed with +.Xr bzip2 1 . +.Ss Diff Mode +The +.Cm diff +mode compares the versions of files in the destination directory to the +.Dq current +tree and generates a unified format diff of the changes. +This can be used to determine which files have been locally modified and how. +Note that +.Nm +does not manage files that are not maintained in the source tree such as +.Pa /etc/fstab +and +.Pa /etc/rc.conf . +.Ss Extract Mode +The +.Cm extract +mode generates a new +.Dq current +tree. +Unlike the default mode, +it does not save any existing +.Dq current +tree and does not modify any existing +.Dq previous +tree. +The new +.Dq current +tree can either be built from a source tree or extracted from a tarball. +.Ss Resolve Mode +The +.Cm resolve +mode is used to resolve any conflicts encountered during a merge. +In this mode, +.Nm +iterates over any existing conflicts prompting the user for actions to take +on each conflicted file. +For each file, the following actions are available: +.Pp +.Bl -tag -width "(tf) theirs-full" -compact +.It (p) postpone +Ignore this conflict for now. +.It (df) diff-full +Show all changes made to the merged file as a unified diff. +.It (e) edit +Change the merged file in an editor. +.It (r) resolved +Install the merged version of the file into the destination directory. +.It (mf) mine-full +Use the version of the file in the destination directory and ignore any +changes made to the file in the +.Dq current +tree. +.It (tf) theirs-full +Use the version of the file from the +.Dq current +tree and discard any local changes made to the file. +.It (h) help +Display the list of commands. +.El +.Ss Revert Mode +The +.Cm revert +mode is used to restore the stock versions of files. +In this mode, +.Nm +installs the stock version of requested files. +This mode cannot be used to restore directories, only individual files. +.Ss Status Mode +The +.Cm status +mode shows a summary of the results of the most recent merge. +First it lists any files for which there are unresolved conflicts. +Next it lists any warnings generated during the last merge. +If the last merge did not generate any conflicts or warnings, +then nothing will be output. +.Sh OPTIONS +The following options are available. +Note that most options do not apply to all modes. +.Bl -tag -width ".Fl A Ar patterns" +.It Fl A Ar patterns +Always install the new version of any files that match any of the patterns +listed in +.Ar patterns . +Each pattern is evaluated as an +.Xr sh 1 +shell pattern. +This option may be specified multiple times to specify multiple patterns. +Multiple space-separated patterns may also be specified in a single +option. +Note that ignored files specified via the +.Ev IGNORE_FILES +variable or the +.Fl I +option will not be installed. +.It Fl B +Do not build generated files in a private object tree. +Instead, +reuse the generated files from a previously built object tree that matches +the source tree. +This can be useful to avoid gratuitous conflicts in +.Xr sendmail 8 +configuration +files when bootstrapping. +It can also be useful for building a tarball that matches a specific +world build. +.It Fl D Ar destdir +Specify an alternate destination directory as the target of a merge. +This is analogous to the +.Dv DESTDIR +variable used with +.Sq make installworld . +The default destination directory is an empty string which results in +merges updating +.Pa /etc +on the local machine. +.It Fl d Ar workdir +Specify an alternate directory to use as the work directory. +The work directory is used to store the +.Dq current +and +.Dq previous +trees as well as unresolved conflicts. +The default work directory is +.Pa <destdir>/var/db/etcupdate . +.It Fl F +Ignore changes in the FreeBSD ID string when comparing files in the +destination directory to files in either of the +.Dq current +or +.Dq previous +trees. +In +.Cm diff +mode, +this reduces noise due to FreeBSD ID string changes in the output. +During an update this can simplify handling for harmless conflicts caused +by FreeBSD ID string changes. +.Pp +Specifically, +if a file in the destination directory is identical to the same file in the +.Dq previous +tree modulo the FreeBSD ID string, +then the file is treated as if it was unmodified and the +.Dq current +version of the file will be installed. +Similarly, +if a file in the destination directory is identical to the same file in the +.Dq current +tree modulo the FreeBSD ID string, +then the +.Dq current +version of the file will be installed to update the ID string. +If the +.Dq previous +and +.Dq current +versions of the file are identical, +then +.Nm +will not change the file in the destination directory. +.Pp +Due to limitations in the +.Xr diff 1 +command, +this option may not have an effect if there are other changes in a file that +are close to the FreeBSD ID string. +.It Fl I Ar patterns +Ignore any files that match any of the patterns listed in +.Ar patterns . +No warnings or other messages will be generated for those files during a +merge. +Each pattern is evaluated as an +.Xr sh 1 +shell pattern. +This option may be specified multiple times to specify multiple patterns. +Multiple space-separated patterns may also be specified in a single +option. +.It Fl L Ar logfile +Specify an alternate path for the log file. +The +.Nm +utility logs each command that it invokes along with the standard output +and standard error to this file. +By default the log file is stored in a file named +.Pa log +in the work directory. +.It Fl M Ar options +Pass +.Ar options +as additional parameters to +.Xr make 1 +when building a +.Dq current +tree. +This can be used to set the +.Dv TARGET +or +.Dv TARGET_ARCH +variables for a cross-build. +.It Fl m Ar make +Use +.Ar make +as the +.Xr make 1 +binary when building a +.Dq current +tree. +.It Fl n +Enable +.Dq dry-run +mode. +Do not merge any changes to the destination directory. +Instead, +report what actions would be taken during a merge. +Note that the existing +.Dq current +and +.Dq previous +trees will not be changed. +If the +.Fl r +option is not specified, +then a temporary +.Dq current +tree will be extracted to perform the comparison. +.It Fl N +Perform a +.Dv NO_ROOT +build when building a +.Dq current +tree. +The resulting tree will include a corresponding +.Pa METALOG +file at its root. +.It Fl p +Enable +.Dq pre-world +mode. +Only merge changes to files that are necessary to successfully run +.Sq make installworld +or +.Sq make installkernel . +When this flag is enabled, +the existing +.Dq current +and +.Dq previous +trees are left alone. +Instead, +a temporary tree is populated with the necessary files. +This temporary tree is compared against the +.Dq current +tree. +This allows a normal update to be run after +.Sq make installworld +has completed. +Any conflicts generated during a +.Dq pre-world +update should be resolved by a +.Dq pre-world +.Cm resolve . +.It Fl r +Do not update the +.Dq current +and +.Dq previous +trees during a merge. +This can be used to +.Dq re-run +a previous merge operation. +.It Fl s Ar source +Specify an alternate source tree to use when building or extracting a +.Dq current +tree. +The default source tree is +.Pa /usr/src . +.It Fl t Ar tarball +Extract a new +.Dq current +tree from a tarball previously generated by the +.Cm build +command rather than building the tree from a source tree. +.El +.Sh CONFIG FILE +The +.Nm +utility can also be configured by setting variables in an optional +configuration file named +.Pa /etc/etcupdate.conf . +Note that command line options override settings in the configuration file. +The configuration file is executed by +.Xr sh 1 , +so it uses that syntax to set configuration variables. +The following variables can be set: +.Bl -tag -width ".Ev ALWAYS_INSTALL" +.It Ev ALWAYS_INSTALL +Always install files that match any of the patterns listed in this variable +similar to the +.Fl A +option. +.It Ev DESTDIR +Specify an alternate destination directory similar to the +.Fl D +option. +.It Ev EDITOR +Specify a program to edit merge conflicts. +.It Ev FREEBSD_ID +Ignore changes in the FreeBSD ID string similar to the +.Fl F +option. +This is enabled by setting the variable to a non-empty value. +.It Ev IGNORE_FILES +Ignore files that match any of the patterns listed in this variable +similar to the +.Fl I +option. +.It Ev LOGFILE +Specify an alternate path for the log file similar to the +.Fl L +option. +.It Ev MAKE_CMD +Specify the +.Xr make 1 +binary when building a +.Dq current +tree similar to the +.Fl m +option. +.It Ev MAKE_OPTIONS +Pass additional options to +.Xr make 1 +when building a +.Dq current +tree similar to the +.Fl M +option. +.It Ev SRCDIR +Specify an alternate source tree similar to the +.Fl s +option. +.It Ev WORKDIR +Specify an alternate work directory similar to the +.Fl d +option. +.El +.Sh ENVIRONMENT +The +.Nm +utility uses the program identified in the +.Ev EDITOR +environment variable to edit merge conflicts. +If +.Ev EDITOR +is not set, +.Xr vi 1 +is used as the default editor. +.Sh FILES +.Bl -tag -width ".Pa /var/db/etcupdate/log" -compact +.It Pa /etc/etcupdate.conf +Optional config file. +.It Pa /var/db/etcupdate +Default work directory used to store trees and other data. +.It Pa /var/db/etcupdate/log +Default log file. +.El +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +To compare the files in +.Pa /etc +with the stock versions: +.Pp +.Dl "etcupdate diff" +.Pp +To merge changes after an upgrade via the buildworld and installworld process: +.Pp +.Dl "etcupdate" +.Pp +To resolve any conflicts generated during a merge: +.Pp +.Dl "etcupdate resolve" +.Ss Bootstrapping +The +.Nm +utility may need to be bootstrapped before it can be used. +The +.Cm diff +command will fail with an error about a missing reference tree if +bootstrapping is needed. +.Pp +Bootstrapping +.Nm +requires a source tree that matches the currently installed world. +The easiest way to ensure this is to bootstrap +.Nm +before updating the source tree to start the next world upgrade cycle. +First, +generate a reference tree: +.Pp +.Dl "etcupdate extract" +.Pp +Second, +use the +.Cm diff +command to compare the reference tree to your current files in +.Pa /etc . +Undesired differences should be removed using an editor, +.Xr patch 1 , +or by copying files from the reference tree +.Po +located at +.Pa /var/db/etcupdate/current +by default +.Pc +. +.Pp +If the tree at +.Pa /usr/src +is already newer than the currently installed world, +a new tree matching the currently installed world can be checked out to +a temporary location. +The reference tree for +.Nm +can then be generated via: +.Pp +.Dl "etcupdate extract -s /path/to/tree" +.Pp +The +.Cm diff +command can be used as above to remove undesired differences. +Afterwards, +the changes in the tree at +.Pa /usr/src +can be merged via a regular merge. +.Sh DIAGNOSTICS +The following warning messages may be generated during a merge. +Note that several of these warnings cover obscure cases that should occur +rarely if at all in practice. +For example, +if a file changes from a file to a directory in the +.Dq current +tree +and the file was modified in the destination directory, +then a warning will be triggered. +In general, +when a warning references a pathname, +the corresponding file in the destination directory is not changed by a +merge operation. +.Bl -diag +.It "Directory mismatch: <path> (<type>)" +An attempt was made to create a directory at +.Pa path +but an existing file of type +.Dq type +already exists for that path name. +.It "Modified link changed: <file> (<old> became <new>)" +The target of a symbolic link named +.Pa file +was changed from +.Dq old +to +.Dq new +in the +.Dq current +tree. +The symbolic link has been modified to point to a target that is neither +.Dq old +nor +.Dq new +in the destination directory. +.It "Modified mismatch: <file> (<new> vs <dest>)" +A file named +.Pa file +of type +.Dq new +was modified in the +.Dq current +tree, +but the file exists as a different type +.Dq dest +in the destination directory. +.It "Modified <type> changed: <file> (<old> became <new>)" +A file named +.Pa file +changed type from +.Dq old +in the +.Dq previous +tree to type +.Dq new +in the +.Dq current +tree. +The file in the destination directory of type +.Dq type +has been modified, +so it could not be merged automatically. +.It "Modified <type> remains: <file>" +The file of type +.Dq type +named +.Pa file +has been removed from the +.Dq current +tree, +but it has been locally modified. +The modified version of the file remains in the destination directory. +.It "Needs update: /etc/localtime (required manual update via tzsetup(8))" +The +.Fa /var/db/zoneinfo +file does not exist, +so +.Nm +was not able to refresh +.Fa /etc/localtime +from its source file in +.Fa /usr/share/zoneinfo . +Running +.Xr tzsetup 8 +will both refresh +.Fa /etc/localtime +and generate +.Fa /var/db/zoneinfo +permitting future updates to refresh +.Fa /etc/localtime +automatically. +.It "Needs update: /etc/mail/aliases.db (required manual update via newaliases(1))" +The file +.Pa /etc/mail/aliases +was updated during a merge with a non-empty destination directory. +Due to a limitation of the +.Xr newaliases 1 +command, +.Nm +was not able to automatically update the corresponding aliases database. +.It "New file mismatch: <file> (<new> vs <dest>)" +A new file named +.Pa file +of type +.Dq new +has been added to the +.Dq current +tree. +A file of that name already exists in the destination directory, +but it is of a different type +.Dq dest . +.It "New link conflict: <file> (<new> vs <dest>)" +A symbolic link named +.Pa file +has been added to the +.Dq current +tree that links to +.Dq new . +A symbolic link of the same name already exists in the destination +directory, +but it links to a different target +.Dq dest . +.It "Non-empty directory remains: <file>" +The directory +.Pa file +was removed from the +.Dq current +tree, +but it contains additional files in the destination directory. +These additional files as well as the directory remain. +.It "Remove mismatch: <file> (<old> became <new>)" +A file named +.Pa file +changed from type +.Dq old +in the +.Dq previous +tree to type +.Dq new +in the +.Dq current +tree, +but it has been removed in the destination directory. +.It "Removed file changed: <file>" +A file named +.Pa file +was modified in the +.Dq current +tree, +but it has been removed in the destination directory. +.It "Removed link changed: <file> (<old> became <new>)" +The target of a symbolic link named +.Pa file +was changed from +.Dq old +to +.Dq new +in the +.Dq current +tree, +but it has been removed in the destination directory. +.El +.Sh SEE ALSO +.Xr cap_mkdb 1 , +.Xr diff 1 , +.Xr make 1 , +.Xr newaliases 1 , +.Xr sh 1 , +.Xr pwd_mkdb 8 , +.Xr services_mkdb 8 , +.Xr tzsetup 8 +.Sh HISTORY +The +.Nm +utility first appeared in +.Fx 10.0 . +.Sh AUTHORS +The +.Nm +utility was written by +.An John Baldwin Aq Mt jhb@FreeBSD.org . +.Sh BUGS +Rerunning a merge does not automatically delete conflicts left over from a +previous merge. +Any conflicts must be resolved before the merge can be rerun. +It is not clear if this is a feature or a bug. +.Pp +There is no way to easily automate conflict resolution for specific files. +For example, one can imagine a syntax along the lines of +.Pp +.Dl "etcupdate resolve tf /some/file" +.Pp +to resolve a specific conflict in an automated fashion. +.Pp +Bootstrapping +.Nm +often results in gratuitous diffs in +.Pa /etc/mail/*.cf +that cause conflicts in the first merge. +If an object tree that matches the source tree is present when bootstrapping, +then passing the +.Fl B +flag to the +.Cm extract +command can work around this. diff --git a/usr.sbin/etcupdate/etcupdate.sh b/usr.sbin/etcupdate/etcupdate.sh new file mode 100755 index 000000000000..7bddd6593b56 --- /dev/null +++ b/usr.sbin/etcupdate/etcupdate.sh @@ -0,0 +1,1963 @@ +#!/bin/sh +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2010-2013 Hudson River Trading LLC +# Written by: John H. Baldwin <jhb@FreeBSD.org> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# + +# This is a tool to manage updating files that are not updated as part +# of 'make installworld' such as files in /etc. Unlike other tools, +# this one is specifically tailored to assisting with mass upgrades. +# To that end it does not require user intervention while running. +# +# Theory of operation: +# +# The most reliable way to update changes to files that have local +# modifications is to perform a three-way merge between the original +# unmodified file, the new version of the file, and the modified file. +# This requires having all three versions of the file available when +# performing an update. +# +# To that end, etcupdate uses a strategy where the current unmodified +# tree is kept in WORKDIR/current and the previous unmodified tree is +# kept in WORKDIR/old. When performing a merge, a new tree is built +# if needed and then the changes are merged into DESTDIR. Any files +# with unresolved conflicts after the merge are left in a tree rooted +# at WORKDIR/conflicts. +# +# To provide extra flexibility, etcupdate can also build tarballs of +# root trees that can later be used. It can also use a tarball as the +# source of a new tree instead of building it from /usr/src. + +# Global settings. These can be adjusted by config files and in some +# cases by command line options. + +# TODO: +# - automatable conflict resolution + +usage() +{ + cat <<EOF +usage: etcupdate [-npBFN] [-d workdir] [-r | -s source | -t tarball] + [-A patterns] [-D destdir] [-I patterns] [-L logfile] + [-M options] [-m make] + etcupdate build [-BN] [-d workdir] [-s source] [-L logfile] [-M options] + [-m make] <tarball> + etcupdate diff [-d workdir] [-D destdir] [-I patterns] [-L logfile] + etcupdate extract [-BN] [-d workdir] [-s source | -t tarball] + [-D destdir] [-L logfile] [-M options] [-m make] + etcupdate resolve [-p] [-d workdir] [-D destdir] [-L logfile] + etcupdate revert [-d workdir] [-D destdir] [-L logfile] file ... + etcupdate status [-d workdir] [-D destdir] +EOF + exit 1 +} + +# Used to write a message prepended with '>>>' to the logfile. +log() +{ + echo ">>>" "$@" >&3 +} + +# Used for assertion conditions that should never happen. +panic() +{ + echo "PANIC:" "$@" + exit 10 +} + +# Used to write a warning message. These are saved to the WARNINGS +# file with " " prepended. +warn() +{ + echo -n " " >> $WARNINGS + echo "$@" >> $WARNINGS +} + +# Output a horizontal rule using the passed-in character. Matches the +# length used for Index lines in CVS and SVN diffs. +# +# $1 - character +rule() +{ + jot -b "$1" -s "" 67 +} + +# Output a text description of a specified file's type. +# +# $1 - file pathname. +file_type() +{ + stat -f "%HT" $1 | tr "[:upper:]" "[:lower:]" +} + +# Returns true (0) if a file exists +# +# $1 - file pathname. +exists() +{ + [ -e $1 -o -L $1 ] +} + +# Returns true (0) if a file should be ignored, false otherwise. +# +# $1 - file pathname +ignore() +{ + local pattern - + + set -o noglob + for pattern in $IGNORE_FILES; do + set +o noglob + case $1 in + $pattern) + return 0 + ;; + esac + set -o noglob + done + + # Ignore /.cshrc and /.profile if they are hardlinked to the + # same file in /root. This ensures we only compare those + # files once in that case. + case $1 in + /.cshrc|/.profile) + if [ ${DESTDIR}$1 -ef ${DESTDIR}/root$1 ]; then + return 0 + fi + ;; + *) + ;; + esac + + return 1 +} + +# Returns true (0) if the new version of a file should always be +# installed rather than attempting to do a merge. +# +# $1 - file pathname +always_install() +{ + local pattern - + + set -o noglob + for pattern in $ALWAYS_INSTALL; do + set +o noglob + case $1 in + $pattern) + return 0 + ;; + esac + set -o noglob + done + + return 1 +} + +# Build a new tree. This runs inside a subshell to trap SIGINT. +# +# $1 - directory to store new tree in +build_tree() +( + local destdir dir file make autogenfiles metatmp + + make="$MAKE_CMD $MAKE_OPTIONS -DNO_FILEMON" + + if [ -n "$noroot" ]; then + make="$make -DNO_ROOT" + metatmp=`mktemp $WORKDIR/etcupdate-XXXXXXX` + : > $metatmp + trap "rm -f $metatmp; trap '' EXIT; return 1" INT + trap "rm -f $metatmp" EXIT + else + metatmp="/dev/null" + trap "return 1" INT + fi + + log "Building tree at $1 with $make" + + exec >&3 2>&1 + + mkdir -p $1/usr/obj + destdir=`realpath $1` + + if [ -n "$preworld" ]; then + # Build a limited tree that only contains files that are + # crucial to installworld. + for file in $PREWORLD_FILES; do + name=$(basename $file) + mkdir -p $1/etc || return 1 + cp -p $SRCDIR/$file $1/etc/$name || return 1 + done + else + ( + cd $SRCDIR || exit 1 + if ! [ -n "$nobuild" ]; then + export MAKEOBJDIRPREFIX=$destdir/usr/obj + if [ -n "$($make -V.ALLTARGETS:Mbuildetc)" ]; then + $make buildetc || exit 1 + else + $make _obj SUBDIR_OVERRIDE=etc || exit 1 + $make everything SUBDIR_OVERRIDE=etc || exit 1 + fi + fi + if [ -n "$($make -V.ALLTARGETS:Minstalletc)" ]; then + $make DESTDIR=$destdir installetc || exit 1 + else + $make DESTDIR=$destdir distrib-dirs || exit 1 + $make DESTDIR=$destdir distribution || exit 1 + fi + ) || return 1 + fi + chflags -R noschg $1 || return 1 + rm -rf $1/usr/obj || return 1 + + # Purge auto-generated files. Only the source files need to + # be updated after which these files are regenerated. + autogenfiles="./etc/*.db ./etc/passwd ./var/db/services.db" + (cd $1 && printf '%s\n' $autogenfiles >> $metatmp && \ + rm -f $autogenfiles) || return 1 + + # Remove empty files. These just clutter the output of 'diff'. + (cd $1 && find . -type f -size 0 -delete -print >> $metatmp) || \ + return 1 + + # Trim empty directories. + (cd $1 && find . -depth -type d -empty -delete -print >> $metatmp) || \ + return 1 + + if [ -n "$noroot" ]; then + # Rewrite the METALOG to exclude the files (and directories) + # removed above. $metatmp contains the list of files to delete, + # and we append #METALOG# as a delimiter followed by the + # original METALOG. This lets us scan through $metatmp in awk + # building up a table of names to delete until we reach the + # delimiter, then emit all the entries of the original METALOG + # after it that aren't in that table. We also exclude ./usr/obj + # and its children explicitly for simplicity rather than + # building up that list (and in practice only ./usr/obj itself + # will be in the METALOG since nothing is installed there). + echo '#METALOG#' >> $metatmp || return 1 + cat $1/METALOG >> $metatmp || return 1 + awk '/^#METALOG#$/ { metalog = 1; next } + { f=$1; gsub(/\/\/+/, "/", f) } + !metalog { rm[f] = 1; next } + !rm[f] && f !~ /^\.\/usr\/obj(\/|$)/ { print }' \ + $metatmp > $1/METALOG || return 1 + fi + + return 0 +) + +# Generate a new tree. If tarball is set, then the tree is +# extracted from the tarball. Otherwise the tree is built from a +# source tree. +# +# $1 - directory to store new tree in +extract_tree() +{ + local files + + # If we have a tarball, extract that into the new directory. + if [ -n "$tarball" ]; then + files= + if [ -n "$preworld" ]; then + files="$PREWORLD_FILES" + fi + if ! (mkdir -p $1 && tar xf $tarball -C $1 $files) \ + >&3 2>&1; then + echo "Failed to extract new tree." + remove_tree $1 + exit 1 + fi + else + if ! build_tree $1; then + echo "Failed to build new tree." + remove_tree $1 + exit 1 + fi + fi +} + +# Forcefully remove a tree. Returns true (0) if the operation succeeds. +# +# $1 - path to tree +remove_tree() +{ + + rm -rf $1 >&3 2>&1 + if [ -e $1 ]; then + chflags -R noschg $1 >&3 2>&1 + rm -rf $1 >&3 2>&1 + fi + [ ! -e $1 ] +} + +# Return values for compare() +COMPARE_EQUAL=0 +COMPARE_ONLYFIRST=1 +COMPARE_ONLYSECOND=2 +COMPARE_DIFFTYPE=3 +COMPARE_DIFFLINKS=4 +COMPARE_DIFFFILES=5 + +# Compare two files/directories/symlinks. Note that this does not +# recurse into subdirectories. Instead, if two nodes are both +# directories, they are assumed to be equivalent. +# +# Returns true (0) if the nodes are identical. If only one of the two +# nodes are present, return one of the COMPARE_ONLY* constants. If +# the nodes are different, return one of the COMPARE_DIFF* constants +# to indicate the type of difference. +# +# $1 - first node +# $2 - second node +compare() +{ + local first second + + # If the first node doesn't exist, then check for the second + # node. Note that -e will fail for a symbolic link that + # points to a missing target. + if ! exists $1; then + if exists $2; then + return $COMPARE_ONLYSECOND + else + return $COMPARE_EQUAL + fi + elif ! exists $2; then + return $COMPARE_ONLYFIRST + fi + + # If the two nodes are different file types fail. + first=`stat -f "%Hp" $1` + second=`stat -f "%Hp" $2` + if [ "$first" != "$second" ]; then + return $COMPARE_DIFFTYPE + fi + + # If both are symlinks, compare the link values. + if [ -L $1 ]; then + first=`readlink $1` + second=`readlink $2` + if [ "$first" = "$second" ]; then + return $COMPARE_EQUAL + else + return $COMPARE_DIFFLINKS + fi + fi + + # If both are files, compare the file contents. + if [ -f $1 ]; then + if cmp -s $1 $2; then + return $COMPARE_EQUAL + else + return $COMPARE_DIFFFILES + fi + fi + + # As long as the two nodes are the same type of file, consider + # them equivalent. + return $COMPARE_EQUAL +} + +# Returns true (0) if the only difference between two regular files is a +# change in the FreeBSD ID string. +# +# $1 - path of first file +# $2 - path of second file +fbsdid_only() +{ + + diff -qI '\$FreeBSD.*\$' $1 $2 >/dev/null 2>&1 +} + +# This is a wrapper around compare that will return COMPARE_EQUAL if +# the only difference between two regular files is a change in the +# FreeBSD ID string. It only makes this adjustment if the -F flag has +# been specified. +# +# $1 - first node +# $2 - second node +compare_fbsdid() +{ + local cmp + + compare $1 $2 + cmp=$? + + if [ -n "$FREEBSD_ID" -a "$cmp" -eq $COMPARE_DIFFFILES ] && \ + fbsdid_only $1 $2; then + return $COMPARE_EQUAL + fi + + return $cmp +} + +# Returns true (0) if a directory is empty. +# +# $1 - pathname of the directory to check +empty_dir() +{ + local contents + + contents=`ls -A $1` + [ -z "$contents" ] +} + +# Returns true (0) if one directories contents are a subset of the +# other. This will recurse to handle subdirectories and compares +# individual files in the trees. Its purpose is to quiet spurious +# directory warnings for dryrun invocations. +# +# $1 - first directory (sub) +# $2 - second directory (super) +dir_subset() +{ + local contents file + + if ! [ -d $1 -a -d $2 ]; then + return 1 + fi + + # Ignore files that are present in the second directory but not + # in the first. + contents=`ls -A $1` + for file in $contents; do + if ! compare $1/$file $2/$file; then + return 1 + fi + + if [ -d $1/$file ]; then + if ! dir_subset $1/$file $2/$file; then + return 1 + fi + fi + done + return 0 +} + +# Returns true (0) if a directory in the destination tree is empty. +# If this is a dryrun, then this returns true as long as the contents +# of the directory are a subset of the contents in the old tree +# (meaning that the directory would be empty in a non-dryrun when this +# was invoked) to quiet spurious warnings. +# +# $1 - pathname of the directory to check relative to DESTDIR. +empty_destdir() +{ + + if [ -n "$dryrun" ]; then + dir_subset $DESTDIR/$1 $OLDTREE/$1 + return + fi + + empty_dir $DESTDIR/$1 +} + +# Output a diff of two directory entries with the same relative name +# in different trees. Note that as with compare(), this does not +# recurse into subdirectories. If the nodes are identical, nothing is +# output. +# +# $1 - first tree +# $2 - second tree +# $3 - node name +# $4 - label for first tree +# $5 - label for second tree +diffnode() +{ + local first second file old new diffargs + + if [ -n "$FREEBSD_ID" ]; then + diffargs="-I \\\$FreeBSD.*\\\$" + else + diffargs="" + fi + + compare_fbsdid $1/$3 $2/$3 + case $? in + $COMPARE_EQUAL) + ;; + $COMPARE_ONLYFIRST) + echo + echo "Removed: $3" + echo + ;; + $COMPARE_ONLYSECOND) + echo + echo "Added: $3" + echo + ;; + $COMPARE_DIFFTYPE) + first=`file_type $1/$3` + second=`file_type $2/$3` + echo + echo "Node changed from a $first to a $second: $3" + echo + ;; + $COMPARE_DIFFLINKS) + first=`readlink $1/$file` + second=`readlink $2/$file` + echo + echo "Link changed: $file" + rule "=" + echo "-$first" + echo "+$second" + echo + ;; + $COMPARE_DIFFFILES) + echo "Index: $3" + rule "=" + diff -u $diffargs -L "$3 ($4)" $1/$3 -L "$3 ($5)" $2/$3 + ;; + esac +} + +# Run one-off commands after an update has completed. These commands +# are not tied to a specific file, so they cannot be handled by +# post_install_file(). +post_update() +{ + local args + + # None of these commands should be run for a pre-world update. + if [ -n "$preworld" ]; then + return + fi + + # If /etc/localtime exists and is not a symlink and /var/db/zoneinfo + # exists, run tzsetup -r to refresh /etc/localtime. + if [ -f ${DESTDIR}/etc/localtime -a \ + ! -L ${DESTDIR}/etc/localtime ]; then + if [ -f ${DESTDIR}/var/db/zoneinfo ]; then + if [ -n "${DESTDIR}" ]; then + args="-C ${DESTDIR}" + else + args="" + fi + log "tzsetup -r ${args}" + if [ -z "$dryrun" ]; then + tzsetup -r ${args} >&3 2>&1 + fi + else + warn "Needs update: /etc/localtime (required" \ + "manual update via tzsetup(8))" + fi + fi +} + +# Create missing parent directories of a node in a target tree +# preserving the owner, group, and permissions from a specified +# template tree. +# +# $1 - template tree +# $2 - target tree +# $3 - pathname of the node (relative to both trees) +install_dirs() +{ + local args dir + + dir=`dirname $3` + + # Nothing to do if the parent directory exists. This also + # catches the degenerate cases when the path is just a simple + # filename. + if [ -d ${2}$dir ]; then + return 0 + fi + + # If non-directory file exists with the desired directory + # name, then fail. + if exists ${2}$dir; then + # If this is a dryrun and we are installing the + # directory in the DESTDIR and the file in the DESTDIR + # matches the file in the old tree, then fake success + # to quiet spurious warnings. + if [ -n "$dryrun" -a "$2" = "$DESTDIR" ]; then + if compare $OLDTREE/$dir $DESTDIR/$dir; then + return 0 + fi + fi + + args=`file_type ${2}$dir` + warn "Directory mismatch: ${2}$dir ($args)" + return 1 + fi + + # Ensure the parent directory of the directory is present + # first. + if ! install_dirs $1 "$2" $dir; then + return 1 + fi + + # Format attributes from template directory as install(1) + # arguments. + args=`stat -f "-o %Su -g %Sg -m %0Mp%0Lp" $1/$dir` + + log "install -d $args ${2}$dir" + if [ -z "$dryrun" ]; then + install -d $args ${2}$dir >&3 2>&1 + fi + return 0 +} + +# Perform post-install fixups for a file. This largely consists of +# regenerating any files that depend on the newly installed file. +# +# $1 - pathname of the updated file (relative to DESTDIR) +post_install_file() +{ + case $1 in + /etc/mail/aliases) + # Grr, newaliases only works for an empty DESTDIR. + if [ -z "$DESTDIR" ]; then + log "newaliases" + if [ -z "$dryrun" ]; then + newaliases >&3 2>&1 + fi + else + NEWALIAS_WARN=yes + fi + ;; + /usr/share/certs/trusted/* | /usr/share/certs/untrusted/*) + log "certctl rehash" + if [ -z "$dryrun" ]; then + env DESTDIR=${DESTDIR} certctl rehash >&3 2>&1 + fi + ;; + /etc/login.conf) + log "cap_mkdb ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + cap_mkdb ${DESTDIR}$1 >&3 2>&1 + fi + ;; + /etc/master.passwd) + log "pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1 \ + >&3 2>&1 + fi + ;; + /etc/motd) + # /etc/rc.d/motd hardcodes the /etc/motd path. + # Don't warn about non-empty DESTDIR's since this + # change is only cosmetic anyway. + if [ -z "$DESTDIR" ]; then + log "sh /etc/rc.d/motd start" + if [ -z "$dryrun" ]; then + sh /etc/rc.d/motd start >&3 2>&1 + fi + fi + ;; + /etc/services) + log "services_mkdb -q -o $DESTDIR/var/db/services.db" \ + "${DESTDIR}$1" + if [ -z "$dryrun" ]; then + services_mkdb -q -o $DESTDIR/var/db/services.db \ + ${DESTDIR}$1 >&3 2>&1 + fi + ;; + esac +} + +# Install the "new" version of a file. Returns true if it succeeds +# and false otherwise. +# +# $1 - pathname of the file to install (relative to DESTDIR) +install_new() +{ + + if ! install_dirs $NEWTREE "$DESTDIR" $1; then + return 1 + fi + log "cp -Rp ${NEWTREE}$1 ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + cp -Rp ${NEWTREE}$1 ${DESTDIR}$1 >&3 2>&1 + fi + post_install_file $1 + return 0 +} + +# Install the "resolved" version of a file. Returns true if it succeeds +# and false otherwise. +# +# $1 - pathname of the file to install (relative to DESTDIR) +install_resolved() +{ + + # This should always be present since the file is already + # there (it caused a conflict). However, it doesn't hurt to + # just be safe. + if ! install_dirs $NEWTREE "$DESTDIR" $1; then + return 1 + fi + + # Use cat rather than cp to preserve metadata + log "cat ${CONFLICTS}$1 > ${DESTDIR}$1" + cat ${CONFLICTS}$1 > ${DESTDIR}$1 2>&3 + post_install_file $1 + return 0 +} + +# Generate a conflict file when a "new" file conflicts with an +# existing file in DESTDIR. +# +# $1 - pathname of the file that conflicts (relative to DESTDIR) +new_conflict() +{ + + if [ -n "$dryrun" ]; then + return + fi + + install_dirs $NEWTREE $CONFLICTS $1 + diff --changed-group-format='<<<<<<< (local) +%<======= +%>>>>>>>> (stock) +' $DESTDIR/$1 $NEWTREE/$1 > $CONFLICTS/$1 +} + +# Remove the "old" version of a file. +# +# $1 - pathname of the old file to remove (relative to DESTDIR) +remove_old() +{ + log "rm -f ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + rm -f ${DESTDIR}$1 >&3 2>&1 + fi + echo " D $1" +} + +# Update a file that has no local modifications. +# +# $1 - pathname of the file to update (relative to DESTDIR) +update_unmodified() +{ + local new old + + # If the old file is a directory, then remove it with rmdir + # (this should only happen if the file has changed its type + # from a directory to a non-directory). If the directory + # isn't empty, then fail. This will be reported as a warning + # later. + if [ -d $DESTDIR/$1 ]; then + if empty_destdir $1; then + log "rmdir ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + rmdir ${DESTDIR}$1 >&3 2>&1 + fi + else + return 1 + fi + + # If both the old and new files are regular files, leave the + # existing file. This avoids breaking hard links for /.cshrc + # and /.profile. Otherwise, explicitly remove the old file. + elif ! [ -f ${DESTDIR}$1 -a -f ${NEWTREE}$1 ]; then + log "rm -f ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + rm -f ${DESTDIR}$1 >&3 2>&1 + fi + fi + + # If the new file is a directory, note that the old file has + # been removed, but don't do anything else for now. The + # directory will be installed if needed when new files within + # that directory are installed. + if [ -d $NEWTREE/$1 ]; then + if empty_dir $NEWTREE/$1; then + echo " D $file" + else + echo " U $file" + fi + elif install_new $1; then + echo " U $file" + fi + return 0 +} + +# Update the FreeBSD ID string in a locally modified file to match the +# FreeBSD ID string from the "new" version of the file. +# +# $1 - pathname of the file to update (relative to DESTDIR) +update_freebsdid() +{ + local new dest file + + # If the FreeBSD ID string is removed from the local file, + # there is nothing to do. In this case, treat the file as + # updated. Otherwise, if either file has more than one + # FreeBSD ID string, just punt and let the user handle the + # conflict manually. + new=`grep -c '\$FreeBSD.*\$' ${NEWTREE}$1` + dest=`grep -c '\$FreeBSD.*\$' ${DESTDIR}$1` + if [ "$dest" -eq 0 ]; then + return 0 + fi + if [ "$dest" -ne 1 -o "$dest" -ne 1 ]; then + return 1 + fi + + # If the FreeBSD ID string in the new file matches the FreeBSD ID + # string in the local file, there is nothing to do. + new=`grep '\$FreeBSD.*\$' ${NEWTREE}$1` + dest=`grep '\$FreeBSD.*\$' ${DESTDIR}$1` + if [ "$new" = "$dest" ]; then + return 0 + fi + + # Build the new file in three passes. First, copy all the + # lines preceding the FreeBSD ID string from the local version + # of the file. Second, append the FreeBSD ID string line from + # the new version. Finally, append all the lines after the + # FreeBSD ID string from the local version of the file. + file=`mktemp $WORKDIR/etcupdate-XXXXXXX` + awk '/\$FreeBSD.*\$/ { exit } { print }' ${DESTDIR}$1 >> $file + awk '/\$FreeBSD.*\$/ { print }' ${NEWTREE}$1 >> $file + awk '/\$FreeBSD.*\$/ { ok = 1; next } { if (ok) print }' \ + ${DESTDIR}$1 >> $file + + # As an extra sanity check, fail the attempt if the updated + # version of the file has any differences aside from the + # FreeBSD ID string. + if ! fbsdid_only ${DESTDIR}$1 $file; then + rm -f $file + return 1 + fi + + log "cp $file ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + cp $file ${DESTDIR}$1 >&3 2>&1 + fi + rm -f $file + post_install_file $1 + echo " M $1" + return 0 +} + +# Attempt to update a file that has local modifications. This routine +# only handles regular files. If the 3-way merge succeeds without +# conflicts, the updated file is installed. If the merge fails, the +# merged version with conflict markers is left in the CONFLICTS tree. +# +# $1 - pathname of the file to merge (relative to DESTDIR) +merge_file() +{ + local res + + # Try the merge to see if there is a conflict. + diff3 -E -m ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 > /dev/null 2>&3 + res=$? + case $res in + 0) + # No conflicts, so just redo the merge to the + # real file. + log "diff3 -E -m ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1" + if [ -z "$dryrun" ]; then + temp=$(mktemp -t etcupdate) + diff3 -E -m ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 > ${temp} + # Use "cat >" to preserve metadata. + cat ${temp} > ${DESTDIR}$1 + rm -f ${temp} + fi + post_install_file $1 + echo " M $1" + ;; + 1) + # Conflicts, save a version with conflict markers in + # the conflicts directory. + if [ -z "$dryrun" ]; then + install_dirs $NEWTREE $CONFLICTS $1 + log "diff3 -m ${DESTDIR}$1 ${CONFLICTS}$1" + diff3 -m -L "yours" -L "original" -L "new" \ + ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 > \ + ${CONFLICTS}$1 + fi + echo " C $1" + ;; + *) + panic "merge failed with status $res" + ;; + esac +} + +# Returns true if a file contains conflict markers from a merge conflict. +# +# $1 - pathname of the file to resolve (relative to DESTDIR) +has_conflicts() +{ + + egrep -q '^(<{7}|\|{7}|={7}|>{7}) ' $CONFLICTS/$1 +} + +# Attempt to resolve a conflict. The user is prompted to choose an +# action for each conflict. If the user edits the file, they are +# prompted again for an action. The process is very similar to +# resolving conflicts after an update or merge with Perforce or +# Subversion. The prompts are modelled on a subset of the available +# commands for resolving conflicts with Subversion. +# +# $1 - pathname of the file to resolve (relative to DESTDIR) +resolve_conflict() +{ + local command junk + + echo "Resolving conflict in '$1':" + edit= + while true; do + # Only display the resolved command if the file + # doesn't contain any conflicts. + echo -n "Select: (p) postpone, (df) diff-full, (e) edit," + if ! has_conflicts $1; then + echo -n " (r) resolved," + fi + echo + echo -n " (h) help for more options: " + read command + case $command in + df) + diff -u ${DESTDIR}$1 ${CONFLICTS}$1 + ;; + e) + $EDITOR ${CONFLICTS}$1 + ;; + h) + cat <<EOF + (p) postpone - ignore this conflict for now + (df) diff-full - show all changes made to merged file + (e) edit - change merged file in an editor + (r) resolved - accept merged version of file + (mf) mine-full - accept local version of entire file (ignore new changes) + (tf) theirs-full - accept new version of entire file (lose local changes) + (h) help - show this list +EOF + ;; + mf) + # For mine-full, just delete the + # merged file and leave the local + # version of the file as-is. + rm ${CONFLICTS}$1 + return + ;; + p) + return + ;; + r) + # If the merged file has conflict + # markers, require confirmation. + if has_conflicts $1; then + echo "File '$1' still has conflicts," \ + "are you sure? (y/n) " + read junk + if [ "$junk" != "y" ]; then + continue + fi + fi + + if ! install_resolved $1; then + panic "Unable to install merged" \ + "version of $1" + fi + rm ${CONFLICTS}$1 + return + ;; + tf) + # For theirs-full, install the new + # version of the file over top of the + # existing file. + if ! install_new $1; then + panic "Unable to install new" \ + "version of $1" + fi + rm ${CONFLICTS}$1 + return + ;; + *) + echo "Invalid command." + ;; + esac + done +} + +# Handle a file that has been removed from the new tree. If the file +# does not exist in DESTDIR, then there is nothing to do. If the file +# exists in DESTDIR and is identical to the old version, remove it +# from DESTDIR. Otherwise, whine about the conflict but leave the +# file in DESTDIR. To handle directories, this uses two passes. The +# first pass handles all non-directory files. The second pass handles +# just directories and removes them if they are empty. +# +# If -F is specified, and the only difference in the file in DESTDIR +# is a change in the FreeBSD ID string, then remove the file. +# +# $1 - pathname of the file (relative to DESTDIR) +handle_removed_file() +{ + local dest file + + file=$1 + if ignore $file; then + log "IGNORE: removed file $file" + return + fi + + compare_fbsdid $DESTDIR/$file $OLDTREE/$file + case $? in + $COMPARE_EQUAL) + if ! [ -d $DESTDIR/$file ]; then + remove_old $file + fi + ;; + $COMPARE_ONLYFIRST) + panic "Removed file now missing" + ;; + $COMPARE_ONLYSECOND) + # Already removed, nothing to do. + ;; + $COMPARE_DIFFTYPE|$COMPARE_DIFFLINKS|$COMPARE_DIFFFILES) + dest=`file_type $DESTDIR/$file` + warn "Modified $dest remains: $file" + ;; + esac +} + +# Handle a directory that has been removed from the new tree. Only +# remove the directory if it is empty. +# +# $1 - pathname of the directory (relative to DESTDIR) +handle_removed_directory() +{ + local dir + + dir=$1 + if ignore $dir; then + log "IGNORE: removed dir $dir" + return + fi + + if [ -d $DESTDIR/$dir -a -d $OLDTREE/$dir ]; then + if empty_destdir $dir; then + log "rmdir ${DESTDIR}$dir" + if [ -z "$dryrun" ]; then + rmdir ${DESTDIR}$dir >/dev/null 2>&1 + fi + echo " D $dir" + else + warn "Non-empty directory remains: $dir" + fi + fi +} + +# Handle a file that exists in both the old and new trees. If the +# file has not changed in the old and new trees, there is nothing to +# do. If the file in the destination directory matches the new file, +# there is nothing to do. If the file in the destination directory +# matches the old file, then the new file should be installed. +# Everything else becomes some sort of conflict with more detailed +# handling. +# +# $1 - pathname of the file (relative to DESTDIR) +handle_modified_file() +{ + local cmp dest file new newdestcmp old + + file=$1 + if ignore $file; then + log "IGNORE: modified file $file" + return + fi + + compare $OLDTREE/$file $NEWTREE/$file + cmp=$? + if [ $cmp -eq $COMPARE_EQUAL ]; then + return + fi + + if [ $cmp -eq $COMPARE_ONLYFIRST -o $cmp -eq $COMPARE_ONLYSECOND ]; then + panic "Changed file now missing" + fi + + compare $NEWTREE/$file $DESTDIR/$file + newdestcmp=$? + if [ $newdestcmp -eq $COMPARE_EQUAL ]; then + return + fi + + # If the only change in the new file versus the destination + # file is a change in the FreeBSD ID string and -F is + # specified, just install the new file. + if [ -n "$FREEBSD_ID" -a $newdestcmp -eq $COMPARE_DIFFFILES ] && \ + fbsdid_only $NEWTREE/$file $DESTDIR/$file; then + if update_unmodified $file; then + return + else + panic "Updating FreeBSD ID string failed" + fi + fi + + # If the local file is the same as the old file, install the + # new file. If -F is specified and the only local change is + # in the FreeBSD ID string, then install the new file as well. + if compare_fbsdid $OLDTREE/$file $DESTDIR/$file; then + if update_unmodified $file; then + return + fi + fi + + # If the file was removed from the dest tree, just whine. + if [ $newdestcmp -eq $COMPARE_ONLYFIRST ]; then + # If the removed file matches an ALWAYS_INSTALL glob, + # then just install the new version of the file. + if always_install $file; then + log "ALWAYS: adding $file" + if ! [ -d $NEWTREE/$file ]; then + if install_new $file; then + echo " A $file" + fi + fi + return + fi + + # If the only change in the new file versus the old + # file is a change in the FreeBSD ID string and -F is + # specified, don't warn. + if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \ + fbsdid_only $OLDTREE/$file $NEWTREE/$file; then + return + fi + + case $cmp in + $COMPARE_DIFFTYPE) + old=`file_type $OLDTREE/$file` + new=`file_type $NEWTREE/$file` + warn "Remove mismatch: $file ($old became $new)" + ;; + $COMPARE_DIFFLINKS) + old=`readlink $OLDTREE/$file` + new=`readlink $NEWTREE/$file` + warn \ + "Removed link changed: $file (\"$old\" became \"$new\")" + ;; + $COMPARE_DIFFFILES) + warn "Removed file changed: $file" + ;; + esac + return + fi + + # Treat the file as unmodified and force install of the new + # file if it matches an ALWAYS_INSTALL glob. If the update + # attempt fails, then fall through to the normal case so a + # warning is generated. + if always_install $file; then + log "ALWAYS: updating $file" + if update_unmodified $file; then + return + fi + fi + + # If the only change in the new file versus the old file is a + # change in the FreeBSD ID string and -F is specified, just + # update the FreeBSD ID string in the local file. + if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \ + fbsdid_only $OLDTREE/$file $NEWTREE/$file; then + if update_freebsdid $file; then + continue + fi + fi + + # If the file changed types between the old and new trees but + # the files in the new and dest tree are both of the same + # type, treat it like an added file just comparing the new and + # dest files. + if [ $cmp -eq $COMPARE_DIFFTYPE ]; then + case $newdestcmp in + $COMPARE_DIFFLINKS) + new=`readlink $NEWTREE/$file` + dest=`readlink $DESTDIR/$file` + warn \ + "New link conflict: $file (\"$new\" vs \"$dest\")" + return + ;; + $COMPARE_DIFFFILES) + new_conflict $file + echo " C $file" + return + ;; + esac + else + # If the file has not changed types between the old + # and new trees, but it is a different type in + # DESTDIR, then just warn. + if [ $newdestcmp -eq $COMPARE_DIFFTYPE ]; then + new=`file_type $NEWTREE/$file` + dest=`file_type $DESTDIR/$file` + warn "Modified mismatch: $file ($new vs $dest)" + return + fi + fi + + case $cmp in + $COMPARE_DIFFTYPE) + old=`file_type $OLDTREE/$file` + new=`file_type $NEWTREE/$file` + dest=`file_type $DESTDIR/$file` + warn "Modified $dest changed: $file ($old became $new)" + ;; + $COMPARE_DIFFLINKS) + old=`readlink $OLDTREE/$file` + new=`readlink $NEWTREE/$file` + warn \ + "Modified link changed: $file (\"$old\" became \"$new\")" + ;; + $COMPARE_DIFFFILES) + merge_file $file + ;; + esac +} + +# Handle a file that has been added in the new tree. If the file does +# not exist in DESTDIR, simply copy the file into DESTDIR. If the +# file exists in the DESTDIR and is identical to the new version, do +# nothing. Otherwise, generate a diff of the two versions of the file +# and mark it as a conflict. +# +# $1 - pathname of the file (relative to DESTDIR) +handle_added_file() +{ + local cmp dest file new + + file=$1 + if ignore $file; then + log "IGNORE: added file $file" + return + fi + + compare $DESTDIR/$file $NEWTREE/$file + cmp=$? + case $cmp in + $COMPARE_EQUAL) + return + ;; + $COMPARE_ONLYFIRST) + panic "Added file now missing" + ;; + $COMPARE_ONLYSECOND) + # Ignore new directories. They will be + # created as needed when non-directory nodes + # are installed. + if ! [ -d $NEWTREE/$file ]; then + if install_new $file; then + echo " A $file" + fi + fi + return + ;; + esac + + + # Treat the file as unmodified and force install of the new + # file if it matches an ALWAYS_INSTALL glob. If the update + # attempt fails, then fall through to the normal case so a + # warning is generated. + if always_install $file; then + log "ALWAYS: updating $file" + if update_unmodified $file; then + return + fi + fi + + case $cmp in + $COMPARE_DIFFTYPE) + new=`file_type $NEWTREE/$file` + dest=`file_type $DESTDIR/$file` + warn "New file mismatch: $file ($new vs $dest)" + ;; + $COMPARE_DIFFLINKS) + new=`readlink $NEWTREE/$file` + dest=`readlink $DESTDIR/$file` + warn "New link conflict: $file (\"$new\" vs \"$dest\")" + ;; + $COMPARE_DIFFFILES) + # If the only change in the new file versus + # the destination file is a change in the + # FreeBSD ID string and -F is specified, just + # install the new file. + if [ -n "$FREEBSD_ID" ] && \ + fbsdid_only $NEWTREE/$file $DESTDIR/$file; then + if update_unmodified $file; then + return + else + panic \ + "Updating FreeBSD ID string failed" + fi + fi + + new_conflict $file + echo " C $file" + ;; + esac +} + +# Main routines for each command + +# Build a new tree and save it in a tarball. +build_cmd() +{ + local dir tartree + + if [ $# -ne 1 ]; then + echo "Missing required tarball." + echo + usage + fi + + log "build command: $1" + + # Create a temporary directory to hold the tree + dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX` + if [ $? -ne 0 ]; then + echo "Unable to create temporary directory." + exit 1 + fi + if ! build_tree $dir; then + echo "Failed to build tree." + remove_tree $dir + exit 1 + fi + if [ -n "$noroot" ]; then + tartree=@METALOG + else + tartree=. + fi + if ! tar cfj $1 -C $dir $tartree >&3 2>&1; then + echo "Failed to create tarball." + remove_tree $dir + exit 1 + fi + remove_tree $dir +} + +# Output a diff comparing the tree at DESTDIR to the current +# unmodified tree. Note that this diff does not include files that +# are present in DESTDIR but not in the unmodified tree. +diff_cmd() +{ + local file + + if [ $# -ne 0 ]; then + usage + fi + + # Requires an unmodified tree to diff against. + if ! [ -d $NEWTREE ]; then + echo "Reference tree to diff against unavailable." + exit 1 + fi + + # Unfortunately, diff alone does not quite provide the right + # level of options that we want, so improvise. + for file in `(cd $NEWTREE; find .) | sed -e 's/^\.//'`; do + if ignore $file; then + continue + fi + + diffnode $NEWTREE "$DESTDIR" $file "stock" "local" + done +} + +# Just extract a new tree into NEWTREE either by building a tree or +# extracting a tarball. This can be used to bootstrap updates by +# initializing the current "stock" tree to match the currently +# installed system. +# +# Unlike 'update', this command does not rotate or preserve an +# existing NEWTREE, it just replaces any existing tree. +extract_cmd() +{ + + if [ $# -ne 0 ]; then + usage + fi + + log "extract command: tarball=$tarball" + + # Create a temporary directory to hold the tree + dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX` + if [ $? -ne 0 ]; then + echo "Unable to create temporary directory." + exit 1 + fi + + extract_tree $dir + + if [ -d $NEWTREE ]; then + if ! remove_tree $NEWTREE; then + echo "Unable to remove current tree." + remove_tree $dir + exit 1 + fi + fi + + if ! mv $dir $NEWTREE >&3 2>&1; then + echo "Unable to rename temp tree to current tree." + remove_tree $dir + exit 1 + fi +} + +# Resolve conflicts left from an earlier merge. +resolve_cmd() +{ + local conflicts + + if [ $# -ne 0 ]; then + usage + fi + + if ! [ -d $CONFLICTS ]; then + return + fi + + if ! [ -d $NEWTREE ]; then + echo "The current tree is not present to resolve conflicts." + exit 1 + fi + + conflicts=`(cd $CONFLICTS; find . ! -type d) | sed -e 's/^\.//'` + for file in $conflicts; do + resolve_conflict $file + done + + if [ -n "$NEWALIAS_WARN" ]; then + warn "Needs update: /etc/mail/aliases.db" \ + "(requires manual update via newaliases(1))" + echo + echo "Warnings:" + echo " Needs update: /etc/mail/aliases.db" \ + "(requires manual update via newaliases(1))" + fi +} + +# Restore files to the stock version. Only files with a local change +# are restored from the stock version. +revert_cmd() +{ + local cmp file + + if [ $# -eq 0 ]; then + usage + fi + + for file; do + log "revert $file" + + if ! [ -e $NEWTREE/$file ]; then + echo "File $file does not exist in the current tree." + exit 1 + fi + if [ -d $NEWTREE/$file ]; then + echo "File $file is a directory." + exit 1 + fi + + compare $DESTDIR/$file $NEWTREE/$file + cmp=$? + if [ $cmp -eq $COMPARE_EQUAL ]; then + continue + fi + + if update_unmodified $file; then + # If this file had a conflict, clean up the + # conflict. + if [ -e $CONFLICTS/$file ]; then + if ! rm $CONFLICTS/$file >&3 2>&1; then + echo "Failed to remove conflict " \ + "for $file". + fi + fi + fi + done +} + +# Report a summary of the previous merge. Specifically, list any +# remaining conflicts followed by any warnings from the previous +# update. +status_cmd() +{ + + if [ $# -ne 0 ]; then + usage + fi + + if [ -d $CONFLICTS ]; then + (cd $CONFLICTS; find . ! -type d) | sed -e 's/^\./ C /' + fi + if [ -s $WARNINGS ]; then + echo "Warnings:" + cat $WARNINGS + fi +} + +# Perform an actual merge. The new tree can either already exist (if +# rerunning a merge), be extracted from a tarball, or generated from a +# source tree. +update_cmd() +{ + local dir new old + + if [ $# -ne 0 ]; then + usage + fi + + log "update command: rerun=$rerun tarball=$tarball preworld=$preworld" + + if [ `id -u` -ne 0 ]; then + echo "Must be root to update a tree." + exit 1 + fi + + # Enforce a sane umask + umask 022 + + # XXX: Should existing conflicts be ignored and removed during + # a rerun? + + # Trim the conflicts tree. Whine if there is anything left. + if [ -e $CONFLICTS ]; then + find -d $CONFLICTS -type d -empty -delete >&3 2>&1 + rmdir $CONFLICTS >&3 2>&1 + fi + if [ -d $CONFLICTS ]; then + echo "Conflicts remain from previous update, aborting." + exit 1 + fi + + # Save tree names to use for rotation later. + old=$OLDTREE + new=$NEWTREE + if [ -z "$rerun" ]; then + # Extract the new tree to a temporary directory. The + # trees are only rotated after a successful update to + # avoid races if an update command is interrupted + # before it completes. + dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX` + if [ $? -ne 0 ]; then + echo "Unable to create temporary directory." + exit 1 + fi + + # Populate the new tree. + extract_tree $dir + + # Compare the new tree against the previous tree. For + # the preworld case OLDTREE already points to the + # current stock tree. + if [ -z "$preworld" ]; then + OLDTREE=$NEWTREE + fi + NEWTREE=$dir + fi + + if ! [ -d $OLDTREE ]; then + cat <<EOF +No previous tree to compare against, a sane comparison is not possible. +EOF + log "No previous tree to compare against." + if [ -n "$dir" ]; then + if [ -n "$rerun" ]; then + panic "Should not have a temporary directory" + fi + remove_tree $dir + fi + exit 1 + fi + + # Build lists of nodes in the old and new trees. + (cd $OLDTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/old.files + (cd $NEWTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/new.files + + # Split the files up into three groups using comm. + comm -23 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/removed.files + comm -13 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/added.files + comm -12 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/both.files + + # Initialize conflicts and warnings handling. + rm -f $WARNINGS + mkdir -p $CONFLICTS + if ! chmod 0700 ${CONFLICTS}; then + panic "Unable to set permissions on conflicts directory" + fi + + # Ignore removed files for the pre-world case. A pre-world + # update uses a stripped-down tree. + if [ -n "$preworld" ]; then + > $WORKDIR/removed.files + fi + + # The order for the following sections is important. In the + # odd case that a directory is converted into a file, the + # existing subfiles need to be removed if possible before the + # file is converted. Similarly, in the case that a file is + # converted into a directory, the file needs to be converted + # into a directory if possible before the new files are added. + + # First, handle removed files. + for file in `cat $WORKDIR/removed.files`; do + handle_removed_file $file + done + + # For the directory pass, reverse sort the list to effect a + # depth-first traversal. This is needed to ensure that if a + # directory with subdirectories is removed, the entire + # directory is removed if there are no local modifications. + for file in `sort -r $WORKDIR/removed.files`; do + handle_removed_directory $file + done + + # Second, handle files that exist in both the old and new + # trees. + for file in `cat $WORKDIR/both.files`; do + handle_modified_file $file + done + + # Finally, handle newly added files. + for file in `cat $WORKDIR/added.files`; do + handle_added_file $file + done + + if [ -n "$NEWALIAS_WARN" ]; then + warn "Needs update: /etc/mail/aliases.db" \ + "(requires manual update via newaliases(1))" + fi + + # Run any special one-off commands after an update has completed. + post_update + + if [ -s $WARNINGS ]; then + echo "Warnings:" + cat $WARNINGS + fi + + # If this was a dryrun, remove the temporary tree if we built + # a new one. + if [ -n "$dryrun" ]; then + if [ -n "$dir" ]; then + if [ -n "$rerun" ]; then + panic "Should not have a temporary directory" + fi + remove_tree $dir + fi + return + fi + + # Finally, rotate any needed trees. + if [ "$new" != "$NEWTREE" ]; then + if [ -n "$rerun" ]; then + panic "Should not have a temporary directory" + fi + if [ -z "$dir" ]; then + panic "Should have a temporary directory" + fi + + # Rotate the old tree if needed + if [ "$old" != "$OLDTREE" ]; then + if [ -n "$preworld" ]; then + panic "Old tree should be unchanged" + fi + + if ! remove_tree $old; then + echo "Unable to remove previous old tree." + exit 1 + fi + + if ! mv $OLDTREE $old >&3 2>&1; then + echo "Unable to rename old tree." + exit 1 + fi + fi + + # Rotate the new tree. Remove a previous pre-world + # tree if it exists. + if [ -d $new ]; then + if [ -z "$preworld" ]; then + panic "New tree should be rotated to old" + fi + if ! remove_tree $new; then + echo "Unable to remove previous pre-world tree." + exit 1 + fi + fi + + if ! mv $NEWTREE $new >&3 2>&1; then + echo "Unable to rename current tree." + exit 1 + fi + fi +} + +# Determine which command we are executing. A command may be +# specified as the first word. If one is not specified then 'update' +# is assumed as the default command. +command="update" +if [ $# -gt 0 ]; then + case "$1" in + build|diff|extract|status|resolve|revert) + command="$1" + shift + ;; + -*) + # If first arg is an option, assume the + # default command. + ;; + *) + usage + ;; + esac +fi + +# Set default variable values. + +# The path to the source tree used to build trees. +SRCDIR=/usr/src + +# The destination directory where the modified files live. +DESTDIR= + +# Ignore changes in the FreeBSD ID string. +FREEBSD_ID= + +# Files that should always have the new version of the file installed. +ALWAYS_INSTALL= + +# Files to ignore and never update during a merge. +IGNORE_FILES= + +# The path to the make binary +MAKE_CMD=make + +# Flags to pass to 'make' when building a tree. +MAKE_OPTIONS= + +# Include a config file if it exists. Note that command line options +# override any settings in the config file. More details are in the +# manual, but in general the following variables can be set: +# - ALWAYS_INSTALL +# - DESTDIR +# - EDITOR +# - FREEBSD_ID +# - IGNORE_FILES +# - LOGFILE +# - MAKE_CMD +# - MAKE_OPTIONS +# - SRCDIR +# - WORKDIR +if [ -r /etc/etcupdate.conf ]; then + . /etc/etcupdate.conf +fi + +# Parse command line options +tarball= +rerun= +always= +dryrun= +ignore= +nobuild= +preworld= +noroot= +while getopts "d:m:nprs:t:A:BD:FI:L:M:N" option; do + case "$option" in + d) + WORKDIR=$OPTARG + ;; + m) + MAKE_CMD=$OPTARG + ;; + n) + dryrun=YES + ;; + p) + preworld=YES + ;; + r) + rerun=YES + ;; + s) + SRCDIR=$OPTARG + ;; + t) + tarball=$OPTARG + ;; + A) + # To allow this option to be specified + # multiple times, accumulate command-line + # specified patterns in an 'always' variable + # and use that to overwrite ALWAYS_INSTALL + # after parsing all options. Need to be + # careful here with globbing expansion. + set -o noglob + always="$always $OPTARG" + set +o noglob + ;; + B) + nobuild=YES + ;; + D) + DESTDIR=$OPTARG + ;; + F) + FREEBSD_ID=YES + ;; + I) + # To allow this option to be specified + # multiple times, accumulate command-line + # specified patterns in an 'ignore' variable + # and use that to overwrite IGNORE_FILES after + # parsing all options. Need to be careful + # here with globbing expansion. + set -o noglob + ignore="$ignore $OPTARG" + set +o noglob + ;; + L) + LOGFILE=$OPTARG + ;; + M) + MAKE_OPTIONS="$OPTARG" + ;; + N) + noroot=YES + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) + +# Allow -A command line options to override ALWAYS_INSTALL set from +# the config file. +set -o noglob +if [ -n "$always" ]; then + ALWAYS_INSTALL="$always" +fi + +# Allow -I command line options to override IGNORE_FILES set from the +# config file. +if [ -n "$ignore" ]; then + IGNORE_FILES="$ignore" +fi +set +o noglob + +# Where the "old" and "new" trees are stored. +WORKDIR=${WORKDIR:-$DESTDIR/var/db/etcupdate} + +# Log file for verbose output from program that are run. The log file +# is opened on fd '3'. +LOGFILE=${LOGFILE:-$WORKDIR/log} + +# The path of the "old" tree +OLDTREE=$WORKDIR/old + +# The path of the "new" tree +NEWTREE=$WORKDIR/current + +# The path of the "conflicts" tree where files with merge conflicts are saved. +CONFLICTS=$WORKDIR/conflicts + +# The path of the "warnings" file that accumulates warning notes from an update. +WARNINGS=$WORKDIR/warnings + +# Use $EDITOR for resolving conflicts. If it is not set, default to vi. +EDITOR=${EDITOR:-/usr/bin/vi} + +# Files that need to be updated before installworld. +PREWORLD_FILES="etc/master.passwd etc/group" + +# Handle command-specific argument processing such as complaining +# about unsupported options. Since the configuration file is always +# included, do not complain about extra command line arguments that +# may have been set via the config file rather than the command line. +case $command in + update) + if [ -n "$rerun" -a -n "$tarball" ]; then + echo "Only one of -r or -t can be specified." + echo + usage + fi + if [ -n "$rerun" -a -n "$preworld" ]; then + echo "Only one of -p or -r can be specified." + echo + usage + fi + ;; + build|diff|status|revert) + if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" -o \ + -n "$preworld" ]; then + usage + fi + ;; + resolve) + if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" ]; then + usage + fi + ;; + extract) + if [ -n "$dryrun" -o -n "$rerun" -o -n "$preworld" ]; then + usage + fi + ;; +esac + +# Pre-world mode uses a different set of trees. It leaves the current +# tree as-is so it is still present for a full etcupdate run after the +# world install is complete. Instead, it installs a few critical files +# into a separate tree. +if [ -n "$preworld" ]; then + OLDTREE=$NEWTREE + NEWTREE=$WORKDIR/preworld +fi + +# Open the log file. Don't truncate it if doing a minor operation so +# that a minor operation doesn't lose log info from a major operation. +if ! mkdir -p $WORKDIR 2>/dev/null; then + echo "Failed to create work directory $WORKDIR" +fi + +case $command in + diff|resolve|revert|status) + exec 3>>$LOGFILE + ;; + *) + exec 3>$LOGFILE + ;; +esac + +${command}_cmd "$@" diff --git a/usr.sbin/etcupdate/tests/Makefile b/usr.sbin/etcupdate/tests/Makefile new file mode 100644 index 000000000000..ba0b6223576b --- /dev/null +++ b/usr.sbin/etcupdate/tests/Makefile @@ -0,0 +1,13 @@ +PLAIN_TESTS_SH= +.for test in always_test \ + conflicts_test \ + fbsdid_test \ + ignore_test \ + preworld_test \ + tests_test \ + tzsetup_test +PLAIN_TESTS_SH+= ${test} +TEST_METADATA.${test}+= required_user="root" +.endfor + +.include <bsd.test.mk> diff --git a/usr.sbin/etcupdate/tests/Makefile.depend b/usr.sbin/etcupdate/tests/Makefile.depend new file mode 100644 index 000000000000..11aba52f82cf --- /dev/null +++ b/usr.sbin/etcupdate/tests/Makefile.depend @@ -0,0 +1,10 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + + +.include <dirdeps.mk> + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/usr.sbin/etcupdate/tests/always_test.sh b/usr.sbin/etcupdate/tests/always_test.sh new file mode 100644 index 000000000000..321f7beb2a67 --- /dev/null +++ b/usr.sbin/etcupdate/tests/always_test.sh @@ -0,0 +1,629 @@ +#!/bin/sh +# +# Copyright (c) 2010 Hudson River Trading LLC +# Written by: John H. Baldwin <jhb@FreeBSD.org> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# + +# Various regression tests to test the -A flag to the 'update' command. + +FAILED=no +WORKDIR=work + +usage() +{ + echo "Usage: always.sh [-s script] [-w workdir]" + exit 1 +} + +# Allow the user to specify an alternate work directory or script. +COMMAND=etcupdate +while getopts "s:w:" option; do + case $option in + s) + COMMAND="sh $OPTARG" + ;; + w) + WORKDIR=$OPTARG + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) +if [ $# -ne 0 ]; then + usage +fi + +CONFLICTS=$WORKDIR/conflicts +OLD=$WORKDIR/old +NEW=$WORKDIR/current +TEST=$WORKDIR/test + +# The various states of the comparison of a file between two trees. +states="equal first second difftype difflinks difffiles" + +# These tests deal with ignoring certain patterns of files. We run +# the test multiple times forcing the install of different patterns. +build_trees() +{ + local i + + rm -rf $OLD $NEW $TEST $CONFLICTS + + for i in $states; do + for j in $states; do + for k in $states; do + mkdir -p $OLD/$i/$j/$k $NEW/$i/$j/$k \ + $TEST/$i/$j/$k + done + done + done + + # What follows are the various warning/conflict cases from the + # larger regression tests. These results of many of these + # tests should be changed when installation is forced. The + # cases when these updates should still fail even when forced + # are: 1) it should not force the removal of a modified file + # and 2) it should not remove a subdirectory that contains a + # modified or added file. + + # /first/difftype/second: File with different local type + # removed. Should generate a warning. + mkfifo $OLD/first/difftype/second/fifo + mkdir $TEST/first/difftype/second/fifo + + # /first/difflinks/second: Modified link removed. Should + # generate a warning. + ln -s "old link" $OLD/first/difflinks/second/link + ln -s "test link" $TEST/first/difflinks/second/link + + # /first/difffiles/second: Modified file removed. Should + # generate a warning. + echo "foo" > $OLD/first/difffiles/second/file + echo "bar" > $TEST/first/difffiles/second/file + + # /second/second/difftype: Newly added file conflicts with + # existing file in test tree of a different type. Should + # generate a warning. + mkdir $NEW/second/second/difftype/dir + mkfifo $TEST/second/second/difftype/dir + + # /second/second/difflinks: Newly added link conflicts with + # existing link in test tree. Should generate a warning. + ln -s "new link" $NEW/second/second/difflinks/link + ln -s "test link" $TEST/second/second/difflinks/link + + # /second/second/difffiles: Newly added file conflicts with + # existing file in test tree. Should generate a warning. + echo "new" > $NEW/second/second/difffiles/file + echo "test" > $TEST/second/second/difffiles/file + + # /difftype/first/first: A removed file has changed type. + # This should generate a warning. + mkfifo $OLD/difftype/first/first/fifo + mkdir $NEW/difftype/first/first/fifo + + # /difftype/difftype/difftype: All three files (old, new, and + # test) are different types from each other. This should + # generate a warning. + mkfifo $OLD/difftype/difftype/difftype/one + mkdir $NEW/difftype/difftype/difftype/one + echo "foo" > $TEST/difftype/difftype/difftype/one + mkdir $OLD/difftype/difftype/difftype/two + echo "baz" > $NEW/difftype/difftype/difftype/two + ln -s "bar" $TEST/difftype/difftype/difftype/two + + # /difftype/difftype/difflinks: A file has changed from a + # non-link to a link in both the new and test trees, but the + # target of the new and test links differ. This should + # generate a new link conflict. + mkfifo $OLD/difftype/difftype/difflinks/link + ln -s "new" $NEW/difftype/difftype/difflinks/link + ln -s "test" $TEST/difftype/difftype/difflinks/link + + # /difftype/difftype/difffile: A file has changed from a + # non-regular file to a regular file in both the new and test + # trees, but the contents in the new and test files differ. + # This should generate a new file conflict. + ln -s "old" $OLD/difftype/difftype/difffiles/file + echo "foo" > $NEW/difftype/difftype/difffiles/file + echo "bar" > $TEST/difftype/difftype/difffiles/file + + # /difflinks/first/first: A modified link is missing in the + # test tree. This should generate a warning. + ln -s "old" $OLD/difflinks/first/first/link + ln -s "new" $NEW/difflinks/first/first/link + + # /difflinks/difftype/difftype: An updated link has been + # changed to a different file type in the test tree. This + # should generate a warning. + ln -s "old" $OLD/difflinks/difftype/difftype/link + ln -s "new" $NEW/difflinks/difftype/difftype/link + echo "test" > $TEST/difflinks/difftype/difftype/link + + # /difflinks/difflinks/difflinks: An updated link has been + # modified in the test tree and doesn't match either the old + # or new links. This should generate a warning. + ln -s "old" $OLD/difflinks/difflinks/difflinks/link + ln -s "new" $NEW/difflinks/difflinks/difflinks/link + ln -s "test" $TEST/difflinks/difflinks/difflinks/link + + # /difffiles/first/first: A removed file has been changed in + # the new tree. This should generate a warning. + echo "foo" > $OLD/difffiles/first/first/file + echo "bar" > $NEW/difffiles/first/first/file + + # /difffiles/difftype/difftype: An updated regular file has + # been changed to a different file type in the test tree. + # This should generate a warning. + echo "old" > $OLD/difffiles/difftype/difftype/file + echo "new" > $NEW/difffiles/difftype/difftype/file + mkfifo $TEST/difffiles/difftype/difftype/file + + # /difffiles/difffiles/difffiles: A modified regular file was + # updated in the new tree. The changes should be merged into + # to the new file if possible. If the merge fails, a conflict + # should be generated. For this test we just include the + # conflict case. + cat > $OLD/difffiles/difffiles/difffiles/conflict <<EOF +this is an old file +EOF + cat > $NEW/difffiles/difffiles/difffiles/conflict <<EOF +this is a new file +EOF + cat > $TEST/difffiles/difffiles/difffiles/conflict <<EOF +this is a test file +EOF + + ## Tests for adding directories + mkdir -p $OLD/adddir $NEW/adddir $TEST/adddir + + # /adddir/conflict: Add a new file in a directory that already + # exists as a file. This should generate two warnings. + mkdir $NEW/adddir/conflict + touch $NEW/adddir/conflict/newfile + touch $TEST/adddir/conflict + + ## Tests for removing directories + mkdir -p $OLD/rmdir $NEW/rmdir $TEST/rmdir + + # /rmdir/extra: Do not remove a directory with an extra local file. + # This should generate a warning. + for i in $OLD $TEST; do + mkdir $i/rmdir/extra + done + echo "foo" > $TEST/rmdir/extra/localfile.txt + + # /rmdir/conflict: Do not remove a directory with a conflicted + # remove file. This should generate a warning. + for i in $OLD $TEST; do + mkdir $i/rmdir/conflict + done + mkfifo $OLD/rmdir/conflict/difftype + mkdir $TEST/rmdir/conflict/difftype + + ## Tests for converting files to directories and vice versa + for i in $OLD $NEW $TEST; do + for j in already old fromdir todir; do + mkdir -p $i/dirchange/$j + done + done + + # /dirchange/fromdir/extradir: Convert a directory tree to a + # file. The test tree includes an extra file in the directory + # that is not present in the old tree. This should generate a + # warning. + for i in $OLD $TEST; do + mkdir $i/dirchange/fromdir/extradir + echo "foo" > $i/dirchange/fromdir/extradir/file + done + mkfifo $TEST/dirchange/fromdir/extradir/fifo + ln -s "bar" $NEW/dirchange/fromdir/extradir + + # /dirchange/fromdir/conflict: Convert a directory tree to a + # file. The test tree includes a local change that generates + # a warning and prevents the removal of the directory. + for i in $OLD $TEST; do + mkdir $i/dirchange/fromdir/conflict + done + echo "foo" > $OLD/dirchange/fromdir/conflict/somefile + echo "bar" > $TEST/dirchange/fromdir/conflict/somefile + mkfifo $NEW/dirchange/fromdir/conflict + + # /dirchange/todir/difffile: Convert a file to a directory + # tree. The test tree has a locally modified version of the + # file so that the conversion fails with a warning. + echo "foo" > $OLD/dirchange/todir/difffile + mkdir $NEW/dirchange/todir/difffile + echo "baz" > $NEW/dirchange/todir/difffile/file + echo "bar" > $TEST/dirchange/todir/difffile + + # /dirchange/todir/difftype: Similar to the previous test, but + # the conflict is due to a change in the file type. + echo "foo" > $OLD/dirchange/todir/difftype + mkdir $NEW/dirchange/todir/difftype + echo "baz" > $NEW/dirchange/todir/difftype/file + mkfifo $TEST/dirchange/todir/difftype +} + +# $1 - relative path to file that should be missing from TEST +missing() +{ + if [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be missing" + FAILED=yes + fi +} + +# $1 - relative path to file that should be present in TEST +present() +{ + if ! [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be present" + FAILED=yes + fi +} + +# $1 - relative path to file that should be a fifo in TEST +fifo() +{ + if ! [ -p $TEST/$1 ]; then + echo "File $1 should be a FIFO" + FAILED=yes + fi +} + +# $1 - relative path to file that should be a directory in TEST +dir() +{ + if ! [ -d $TEST/$1 ]; then + echo "File $1 should be a directory" + FAILED=yes + fi +} + +# $1 - relative path to file that should be a symlink in TEST +# $2 - optional value of the link +link() +{ + local val + + if ! [ -L $TEST/$1 ]; then + echo "File $1 should be a link" + FAILED=yes + elif [ $# -gt 1 ]; then + val=`readlink $TEST/$1` + if [ "$val" != "$2" ]; then + echo "Link $1 should link to \"$2\"" + FAILED=yes + fi + fi +} + +# $1 - relative path to regular file that should be present in TEST +# $2 - optional string that should match file contents +# $3 - optional MD5 of the flie contents, overrides $2 if present +file() +{ + local contents sum + + if ! [ -f $TEST/$1 ]; then + echo "File $1 should be a regular file" + FAILED=yes + elif [ $# -eq 2 ]; then + contents=`cat $TEST/$1` + if [ "$contents" != "$2" ]; then + echo "File $1 has wrong contents" + FAILED=yes + fi + elif [ $# -eq 3 ]; then + sum=`md5 -q $TEST/$1` + if [ "$sum" != "$3" ]; then + echo "File $1 has wrong contents" + FAILED=yes + fi + fi +} + +# $1 - relative path to a regular file that should have a conflict +# $2 - optional MD5 of the conflict file contents +conflict() +{ + local sum + + if ! [ -f $CONFLICTS/$1 ]; then + echo "File $1 missing conflict" + FAILED=yes + elif [ $# -gt 1 ]; then + sum=`md5 -q $CONFLICTS/$1` + if [ "$sum" != "$2" ]; then + echo "Conflict $1 has wrong contents" + FAILED=yes + fi + fi +} + +# $1 - relative path to a regular file that should not have a conflict +noconflict() +{ + if [ -f $CONFLICTS/$1 ]; then + echo "File $1 should not have a conflict" + FAILED=yes + fi +} + +if [ `id -u` -ne 0 ]; then + echo "must be root" + exit 0 +fi + +if [ -r /etc/etcupdate.conf ]; then + echo "WARNING: /etc/etcupdate.conf settings may break some tests." +fi + +# First run the test ignoring no patterns. + +build_trees + +$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out + +cat > $WORKDIR/correct.out <<EOF + D /dirchange/fromdir/extradir/file + C /difffiles/difffiles/difffiles/conflict + C /difftype/difftype/difffiles/file + C /second/second/difffiles/file +Warnings: + Modified regular file remains: /dirchange/fromdir/conflict/somefile + Modified regular file remains: /first/difffiles/second/file + Modified symbolic link remains: /first/difflinks/second/link + Modified directory remains: /first/difftype/second/fifo + Modified directory remains: /rmdir/conflict/difftype + Non-empty directory remains: /rmdir/extra + Non-empty directory remains: /rmdir/conflict + Modified mismatch: /difffiles/difftype/difftype/file (regular file vs fifo file) + Removed file changed: /difffiles/first/first/file + Modified link changed: /difflinks/difflinks/difflinks/link ("old" became "new") + Modified mismatch: /difflinks/difftype/difftype/link (symbolic link vs regular file) + Removed link changed: /difflinks/first/first/link ("old" became "new") + New link conflict: /difftype/difftype/difflinks/link ("new" vs "test") + Modified regular file changed: /difftype/difftype/difftype/one (fifo file became directory) + Modified symbolic link changed: /difftype/difftype/difftype/two (directory became regular file) + Remove mismatch: /difftype/first/first/fifo (fifo file became directory) + Modified directory changed: /dirchange/fromdir/conflict (directory became fifo file) + Modified directory changed: /dirchange/fromdir/extradir (directory became symbolic link) + Modified regular file changed: /dirchange/todir/difffile (regular file became directory) + Modified fifo file changed: /dirchange/todir/difftype (regular file became directory) + New file mismatch: /adddir/conflict (directory vs regular file) + Directory mismatch: $TEST/adddir/conflict (regular file) + Directory mismatch: $TEST/dirchange/todir/difffile (regular file) + Directory mismatch: $TEST/dirchange/todir/difftype (fifo file) + New link conflict: /second/second/difflinks/link ("new link" vs "test link") + New file mismatch: /second/second/difftype/dir (directory vs fifo file) +EOF + +echo "Differences for regular:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \ + || FAILED=yes + +## /first/difftype/second: +present /first/difftype/second/fifo + +## /first/difflinks/second: +link /first/difflinks/second/link "test link" + +## /first/difffiles/second: +file /first/difffiles/second/file "bar" + +## /second/second/difftype: +fifo /second/second/difftype/dir + +## /second/second/difflinks: +link /second/second/difflinks/link "test link" + +## /second/second/difffiles: +file /second/second/difffiles/file "test" +conflict /second/second/difffiles/file 4f2ee8620a251fd53f06bb6112eb6ffa + +## /difftype/first/first: +missing /difftype/first/first/fifo + +## /difftype/difftype/difftype: +file /difftype/difftype/difftype/one "foo" +link /difftype/difftype/difftype/two "bar" + +## /difftype/difftype/difflinks: +link /difftype/difftype/difflinks/link "test" + +## /difftype/difftype/difffile: +conflict /difftype/difftype/difffiles/file 117f2bcd1f6491f6044e79e5a57a9229 + +## /difflinks/first/first: +missing /difflinks/first/first/link + +## /difflinks/difftype/difftype: +file /difflinks/difftype/difftype/link "test" + +## /difflinks/difflinks/difflinks: +link /difflinks/difflinks/difflinks/link "test" + +## /difffiles/first/first: +missing /difffiles/first/first/file + +## /difffiles/difftype/difftype: +fifo /difffiles/difftype/difftype/file + +## /difffiles/difffiles/difffiles: +file /difffiles/difffiles/difffiles/conflict "this is a test file" +conflict /difffiles/difffiles/difffiles/conflict \ + 8261cfdd89280c4a6c26e4ac86541fe9 + +## /adddir/conflict: +file /adddir/conflict + +## /rmdir/extra: +dir /rmdir/extra +file /rmdir/extra/localfile.txt "foo" + +## /rmdir/conflict: +dir /rmdir/conflict/difftype +present /rmdir/conflict + +## /dirchange/fromdir/extradir: +missing /dirchange/fromdir/extradir/file +fifo /dirchange/fromdir/extradir/fifo + +## /dirchange/fromdir/conflict: +file /dirchange/fromdir/conflict/somefile "bar" + +## /dirchange/todir/difffile: +file /dirchange/todir/difffile "bar" + +## /dirchange/todir/difftype: +fifo /dirchange/todir/difftype + +# Now test with -A '/first*' -A '/second* /*di*'. This should remove +# most of the warnings and conflicts. + +build_trees + +$COMMAND -r -A '/first*' -A '/second* /*di*' -d $WORKDIR -D $TEST > \ + $WORKDIR/test1.out + +cat > $WORKDIR/correct1.out <<EOF + D /dirchange/fromdir/extradir/file + U /difffiles/difffiles/difffiles/conflict + U /difffiles/difftype/difftype/file + A /difffiles/first/first/file + U /difflinks/difflinks/difflinks/link + U /difflinks/difftype/difftype/link + A /difflinks/first/first/link + U /difftype/difftype/difffiles/file + U /difftype/difftype/difflinks/link + D /difftype/difftype/difftype/one + U /difftype/difftype/difftype/two + U /dirchange/todir/difffile + U /dirchange/todir/difftype + U /adddir/conflict + A /adddir/conflict/newfile + A /dirchange/todir/difffile/file + A /dirchange/todir/difftype/file + U /second/second/difffiles/file + U /second/second/difflinks/link + D /second/second/difftype/dir +Warnings: + Modified regular file remains: /dirchange/fromdir/conflict/somefile + Modified regular file remains: /first/difffiles/second/file + Modified symbolic link remains: /first/difflinks/second/link + Modified directory remains: /first/difftype/second/fifo + Modified directory remains: /rmdir/conflict/difftype + Non-empty directory remains: /rmdir/extra + Non-empty directory remains: /rmdir/conflict + Modified directory changed: /dirchange/fromdir/conflict (directory became fifo file) + Modified directory changed: /dirchange/fromdir/extradir (directory became symbolic link) +EOF + +echo "Differences for -A '/first*' -A '/second* /*di*':" +diff -u -L "correct" $WORKDIR/correct1.out -L "test" $WORKDIR/test1.out \ + || FAILED=yes + +## /first/difftype/second: +present /first/difftype/second/fifo + +## /first/difflinks/second: +link /first/difflinks/second/link "test link" + +## /first/difffiles/second: +file /first/difffiles/second/file "bar" + +## /second/second/difftype: +missing /second/second/difftype/dir + +## /second/second/difflinks: +link /second/second/difflinks/link "new link" + +## /second/second/difffiles: +file /second/second/difffiles/file "new" +noconflict /second/second/difffiles/file + +## /difftype/first/first: +missing /difftype/first/first/fifo + +## /difftype/difftype/difftype: +missing /difftype/difftype/difftype/one +file /difftype/difftype/difftype/two "baz" + +## /difftype/difftype/difflinks: +link /difftype/difftype/difflinks/link "new" + +## /difftype/difftype/difffile: +noconflict /difftype/difftype/difffiles/file +file /difftype/difftype/difffiles/file "foo" + +## /difflinks/first/first: +link /difflinks/first/first/link "new" + +## /difflinks/difftype/difftype: +link /difflinks/difftype/difftype/link "new" + +## /difflinks/difflinks/difflinks: +link /difflinks/difflinks/difflinks/link "new" + +## /difffiles/first/first: +file /difffiles/first/first/file "bar" + +## /difffiles/difftype/difftype: +file /difffiles/difftype/difftype/file "new" + +## /difffiles/difffiles/difffiles: +noconflict /difffiles/difffiles/difffiles/conflict +file /difffiles/difffiles/difffiles/conflict "this is a new file" + +## /adddir/conflict: +file /adddir/conflict/newfile + +## /rmdir/extra: +dir /rmdir/extra +file /rmdir/extra/localfile.txt "foo" + +## /rmdir/conflict: +dir /rmdir/conflict/difftype +present /rmdir/conflict + +## /dirchange/fromdir/extradir: +missing /dirchange/fromdir/extradir/file +fifo /dirchange/fromdir/extradir/fifo + +## /dirchange/fromdir/conflict: +file /dirchange/fromdir/conflict/somefile "bar" + +## /dirchange/todir/difffile: +file /dirchange/todir/difffile/file "baz" + +## /dirchange/todir/difftype: +file /dirchange/todir/difftype/file "baz" + +[ "${FAILED}" = no ] diff --git a/usr.sbin/etcupdate/tests/conflicts_test.sh b/usr.sbin/etcupdate/tests/conflicts_test.sh new file mode 100644 index 000000000000..87857e7cd0a9 --- /dev/null +++ b/usr.sbin/etcupdate/tests/conflicts_test.sh @@ -0,0 +1,293 @@ +#!/bin/sh +# +# Copyright (c) 2010 Hudson River Trading LLC +# Written by: John H. Baldwin <jhb@FreeBSD.org> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# + +# Various regression tests to run for the 'resolve' command. + +FAILED=no +WORKDIR=work + +usage() +{ + echo "Usage: conflicts.sh [-s script] [-w workdir]" + exit 1 +} + +# Allow the user to specify an alternate work directory or script. +COMMAND=etcupdate +while getopts "s:w:" option; do + case $option in + s) + COMMAND="sh $OPTARG" + ;; + w) + WORKDIR=$OPTARG + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) +if [ $# -ne 0 ]; then + usage +fi + +CONFLICTS=$WORKDIR/conflicts +OLD=$WORKDIR/old +NEW=$WORKDIR/current +TEST=$WORKDIR/test + +# These tests deal with conflicts to a single file. For each test, we +# generate a conflict in /etc/login.conf. Each resolve option is tested +# to ensure it DTRT. +build_login_conflict() +{ + + rm -rf $OLD $NEW $TEST $CONFLICTS + mkdir -p $OLD/etc $NEW/etc $TEST/etc + + # Generate a conflict in /etc/login.conf. + cat > $OLD/etc/login.conf <<EOF +default:\\ + :passwd_format=md5: +EOF + cat > $NEW/etc/login.conf <<EOF +default:\\ + :passwd_format=md5:\\ + :copyright=/etc/COPYRIGHT +EOF + cat > $TEST/etc/login.conf <<EOF +default:\\ + :passwd_format=md5:\\ + :welcome=/etc/motd: +EOF + + $COMMAND -r -d $WORKDIR -D $TEST >/dev/null +} + +# This is used to verify special handling for /etc/mail/aliases and +# the newaliases warning. +build_aliases_conflict() +{ + + rm -rf $OLD $NEW $TEST $CONFLICTS + mkdir -p $OLD/etc/mail $NEW/etc/mail $TEST/etc/mail + + # Generate a conflict in /etc/mail/aliases + cat > $OLD/etc/mail/aliases <<EOF +# root: me@my.domain + +# Basic system aliases -- these MUST be present +MAILER-DAEMON: postmaster +postmaster: root +EOF + cat > $NEW/etc/mail/aliases <<EOF +# root: me@my.domain + +# Basic system aliases -- these MUST be present +MAILER-DAEMON: postmaster +postmaster: root + +# General redirections for pseudo accounts +_dhcp: root +_pflogd: root +EOF + cat > $TEST/etc/mail/aliases <<EOF +root: someone@example.com + +# Basic system aliases -- these MUST be present +MAILER-DAEMON: postmaster +postmaster: foo +EOF + + $COMMAND -r -d $WORKDIR -D $TEST >/dev/null +} + +# $1 - relative path to file that should be missing from TEST +missing() +{ + if [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be missing" + FAILED=yes + fi +} + +# $1 - relative path to file that should be present in TEST +present() +{ + if ! [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be present" + FAILED=yes + fi +} + +# $1 - relative path to regular file that should be present in TEST +# $2 - optional string that should match file contents +# $3 - optional MD5 of the flie contents, overrides $2 if present +file() +{ + local contents sum + + if ! [ -f $TEST/$1 ]; then + echo "File $1 should be a regular file" + FAILED=yes + elif [ $# -eq 2 ]; then + contents=`cat $TEST/$1` + if [ "$contents" != "$2" ]; then + echo "File $1 has wrong contents" + FAILED=yes + fi + elif [ $# -eq 3 ]; then + sum=`md5 -q $TEST/$1` + if [ "$sum" != "$3" ]; then + echo "File $1 has wrong contents" + FAILED=yes + fi + fi +} + +# $1 - relative path to a regular file that should have a conflict +# $2 - optional MD5 of the conflict file contents +conflict() +{ + local sum + + if ! [ -f $CONFLICTS/$1 ]; then + echo "File $1 missing conflict" + FAILED=yes + elif [ $# -gt 1 ]; then + sum=`md5 -q $CONFLICTS/$1` + if [ "$sum" != "$2" ]; then + echo "Conflict $1 has wrong contents" + FAILED=yes + fi + fi +} + +# $1 - relative path to a regular file that should no longer have a conflict +resolved() +{ + if [ -f $CONFLICTS/$1 ]; then + echo "Conflict $1 should be resolved" + FAILED=yes + fi +} + +if [ `id -u` -ne 0 ]; then + echo "must be root" + exit 0 +fi + +if [ -r /etc/etcupdate.conf ]; then + echo "WARNING: /etc/etcupdate.conf settings may break some tests." +fi + +# Test each of the following resolve options: 'p', 'mf', 'tf', 'r'. + +build_login_conflict + +# Verify that 'p' doesn't do anything. +echo "Checking 'p':" +echo 'p' | $COMMAND resolve -d $WORKDIR -D $TEST >/dev/null + +file /etc/login.conf "" 95de92ea3f1bb1bf4f612a8b5908cddd +missing /etc/login.conf.db +conflict /etc/login.conf + +# Verify that 'mf' removes the conflict, but does nothing else. +echo "Checking 'mf':" +echo 'mf' | $COMMAND resolve -d $WORKDIR -D $TEST >/dev/null + +file /etc/login.conf "" 95de92ea3f1bb1bf4f612a8b5908cddd +missing /etc/login.conf.db +resolved /etc/login.conf + +build_login_conflict + +# Verify that 'tf' installs the new version of the file. +echo "Checking 'tf':" +echo 'tf' | $COMMAND resolve -d $WORKDIR -D $TEST >/dev/null + +file /etc/login.conf "" 7774a0f9a3a372c7c109c32fd31c4b6b +file /etc/login.conf.db +resolved /etc/login.conf + +build_login_conflict + +# Verify that 'r' installs the resolved version of the file. To +# simulate this, manually edit the merged file so that it doesn't +# contain conflict markers. +echo "Checking 'r':" +cat > $CONFLICTS/etc/login.conf <<EOF +default:\\ + :passwd_format=md5:\\ + :copyright=/etc/COPYRIGHT\\ + :welcome=/etc/motd: +EOF + +echo 'r' | $COMMAND resolve -d $WORKDIR -D $TEST >/dev/null + +file /etc/login.conf "" 966e25984b9b63da8eaac8479dcb0d4d +file /etc/login.conf.db +resolved /etc/login.conf + +build_aliases_conflict + +# Verify that 'p' and 'mf' do not generate the newaliases warning. +echo "Checking newalias warning for 'p'": +echo 'p' | $COMMAND resolve -d $WORKDIR -D $TEST | grep -q newalias +if [ $? -eq 0 ]; then + echo "+ Extra warning" + FAILED=yes +fi +echo "Checking newalias warning for 'mf'": +echo 'mf' | $COMMAND resolve -d $WORKDIR -D $TEST | grep -q newalias +if [ $? -eq 0 ]; then + echo "+ Extra warning" + FAILED=yes +fi + +# Verify that 'tf' and 'r' do generate the newaliases warning. +build_aliases_conflict +echo "Checking newalias warning for 'tf'": +echo 'tf' | $COMMAND resolve -d $WORKDIR -D $TEST | grep -q newalias +if [ $? -ne 0 ]; then + echo "- Missing warning" + FAILED=yes +fi + +build_aliases_conflict +cp $TEST/etc/mail/aliases $CONFLICTS/etc/mail/aliases +echo 'r' | $COMMAND resolve -d $WORKDIR -D $TEST | grep -q newalias +if [ $? -ne 0 ]; then + echo "- Missing warning" + FAILED=yes +fi + +[ "${FAILED}" = no ] diff --git a/usr.sbin/etcupdate/tests/fbsdid_test.sh b/usr.sbin/etcupdate/tests/fbsdid_test.sh new file mode 100644 index 000000000000..b93c2ff1793e --- /dev/null +++ b/usr.sbin/etcupdate/tests/fbsdid_test.sh @@ -0,0 +1,393 @@ +#!/bin/sh +# +# Copyright (c) 2010 Hudson River Trading LLC +# Written by: John H. Baldwin <jhb@FreeBSD.org> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# + +# Various regression tests to test the -F flag to the 'update' command. + +FAILED=no +WORKDIR=work + +usage() +{ + echo "Usage: fbsdid.sh [-s script] [-w workdir]" + exit 1 +} + +# Allow the user to specify an alternate work directory or script. +COMMAND=etcupdate +while getopts "s:w:" option; do + case $option in + s) + COMMAND="sh $OPTARG" + ;; + w) + WORKDIR=$OPTARG + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) +if [ $# -ne 0 ]; then + usage +fi + +CONFLICTS=$WORKDIR/conflicts +OLD=$WORKDIR/old +NEW=$WORKDIR/current +TEST=$WORKDIR/test + +# Store a FreeBSD ID string in a specified file. The first argument +# is the file, the remaining arguments are the comment to use. +store_id() +{ + local file + + file=$1 + shift + + echo -n '# $FreeBSD' >> $file + echo -n "$@" >> $file + echo '$' >> $file +} + +# These tests deal with FreeBSD ID string conflicts. We run the test +# twice, once without -F and once with -F. +build_trees() +{ + local i + + rm -rf $OLD $NEW $TEST $CONFLICTS + mkdir -p $OLD $NEW $TEST + + # remove: Remove a file where the only local difference is a + # change in the FreeBSD ID string. + store_id $OLD/remove + store_id $TEST/remove ": head/remove 12345 jhb " + + # old: Modify a file where the only local difference between + # the old and test files is a change in the FreeBSD ID string. + store_id $OLD/old ": src/old,v 1.1 jhb Exp " + store_id $NEW/old ": head/old 12345 jhb " + store_id $TEST/old ": head/old 12000 jhb " + for i in $OLD $TEST; do + cat >> $i/old <<EOF + +an old file +EOF + done + cat >> $NEW/old <<EOF + +a new file +EOF + + # already: Modify a file where the local file already matches + # the new file except for a change in the FreeBSD ID string. + store_id $OLD/already ": src/already,v 1.1 jhb Exp " + store_id $NEW/already ": head/already 12345 jhb " + store_id $TEST/already ": src/already,v 1.2 jhb Exp " + cat >> $OLD/already <<EOF + +another old file +EOF + for i in $NEW $TEST; do + cat >> $i/already <<EOF + +another new file +EOF + done + + # add: Add a file that already exists where the only local + # difference is a change in the FreeBSD ID string. + store_id $NEW/add ": head/add 12345 jhb " + store_id $TEST/add "" + + # conflict: Modify a file where the local file has a different + # FreeBSD ID string. This should still generate a conflict + # even in the -F case. + store_id $OLD/conflict ": head/conflict 12000 jhb " + store_id $NEW/conflict ": head/conflict 12345 jhb " + store_id $TEST/conflict "" + cat >> $OLD/conflict <<EOF + +this is the old file +EOF + cat >> $NEW/conflict <<EOF + +this is the new file +EOF + cat >> $TEST/conflict <<EOF + +this is the local file +EOF + + # local: A file with local modifications has a different + # FreeBSD ID string and the only differences between the old + # and new versions are a change in the FreeBSD ID string. + # This will just update the FreeBSD ID string in the -F case. + for i in $OLD $NEW $TEST; do + cat >> $i/local <<EOF +# Some leading text +# +EOF + done + + store_id $OLD/local ": head/local 12000 jhb " + store_id $NEW/local ": head/local 12345 jhb " + store_id $TEST/local ": src/local,v 1.5 jhb Exp " + + for i in $OLD $NEW $TEST; do + cat >> $i/local <<EOF + +this is a file +EOF + done + + cat >> $TEST/local <<EOF + +these are some local mods to the file +EOF + + # local-already: A file with local modifications has the same + # FreeBSD ID string as the new version of the file and the + # only differences between the old and new versions are a + # change in the FreeBSD ID string. Nothing should happen in + # the -F case. + store_id $OLD/local-already ": head/local 12000 jhb " + for i in $NEW $TEST; do + store_id $i/local-already ": head/local 12345 jhb " + done + + for i in $OLD $NEW $TEST; do + cat >> $i/local-already <<EOF + +this is a file +EOF + done + + cat >> $TEST/local-already <<EOF + +these are some local mods to the file +EOF + + # local-remove: A file removed locally changed it's FreeBSD ID + # but nothing else + store_id $OLD/local-remove ": head/local-remove 12000 jhb " + store_id $NEW/local-remove ": head/local-remove 12345 jhb " + for i in $OLD $NEW; do + cat >> $i/local-remove <<EOF + +this is a file +EOF + done +} + +# $1 - relative path to file that should be missing from TEST +missing() +{ + if [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be missing" + FAILED=yes + fi +} + +# $1 - relative path to file that should be present in TEST +present() +{ + if ! [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be present" + FAILED=yes + fi +} + +# $1 - relative path to regular file that should be present in TEST +# $2 - optional string that should match file contents +# $3 - optional MD5 of the flie contents, overrides $2 if present +file() +{ + local contents sum + + if ! [ -f $TEST/$1 ]; then + echo "File $1 should be a regular file" + FAILED=yes + elif [ $# -eq 2 ]; then + contents=`cat $TEST/$1` + if [ "$contents" != "$2" ]; then + echo "File $1 has wrong contents" + FAILED=yes + fi + elif [ $# -eq 3 ]; then + sum=`md5 -q $TEST/$1` + if [ "$sum" != "$3" ]; then + echo "File $1 has wrong contents" + FAILED=yes + fi + fi +} + +# $1 - relative path to a regular file that should have a conflict +# $2 - optional MD5 of the conflict file contents +conflict() +{ + local sum + + if ! [ -f $CONFLICTS/$1 ]; then + echo "File $1 missing conflict" + FAILED=yes + elif [ $# -gt 1 ]; then + sum=`md5 -q $CONFLICTS/$1` + if [ "$sum" != "$2" ]; then + echo "Conflict $1 has wrong contents" + FAILED=yes + fi + fi +} + +# $1 - relative path to a regular file that should not have a conflict +noconflict() +{ + if [ -f $CONFLICTS/$1 ]; then + echo "File $1 should not have a conflict" + FAILED=yes + fi +} + +if [ `id -u` -ne 0 ]; then + echo "must be root" + exit 0 +fi + +if [ -r /etc/etcupdate.conf ]; then + echo "WARNING: /etc/etcupdate.conf settings may break some tests." +fi + +# First run the test without -F. + +build_trees + +$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out + +cat > $WORKDIR/correct.out <<EOF + C /already + C /conflict + C /local + M /local-already + C /old + C /add +Warnings: + Modified regular file remains: /remove + Removed file changed: /local-remove +EOF + +echo "Differences for regular:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \ + || FAILED=yes + +file /remove "" 1bb4776213af107077be78fead8a351c +file /old "" 2f799a7addc4132563ef9b44adc66157 +conflict /old 8441be64a1540f2ff584015279682425 +file /already "" aa53bd506f65d01d766e7ba028585e1d +conflict /already f44105abb1fa3293e95c5d77e428d418 +file /add "" 1dc8c617e541d1fd1b4c70212f71d8ae +conflict /add f99081e0da9a07f3cfebb430c0414941 +file /conflict "" dc27978df125b0daeb7d9b93265f03fd +conflict /conflict 868452f666fea1c60ffb918ad9ad9607 +file /local "" aa33e614b5e749449f230e2a2b0072eb +conflict /local 3df93e64043c8e348fc625b93ea220f4 +file /local-already "" 0298b958a603049f45ae6a109c4f7fea +missing /local-remove + +# Now test with -F. + +build_trees + +$COMMAND -rF -d $WORKDIR -D $TEST > $WORKDIR/testF.out + +cat > $WORKDIR/correctF.out <<EOF + D /remove + U /already + C /conflict + M /local + U /old + U /add +EOF + +echo "Differences for -F:" +diff -u -L "correct" $WORKDIR/correctF.out -L "test" $WORKDIR/testF.out \ + || FAILED=yes + +missing /remove +file /old "" 6a9f34f109d94406a4de3bc5d72de259 +noconflict /old +file /already "" 21f4eca3aacc702c49878c8da7afd3d0 +noconflict /already +file /add "" 0208bd647111fedf6318511712ab9e97 +noconflict /add +file /conflict "" dc27978df125b0daeb7d9b93265f03fd +conflict /conflict 868452f666fea1c60ffb918ad9ad9607 +file /local "" 3ed5a35e380c8a93fb5f599d4c052713 +file /local-already "" 0298b958a603049f45ae6a109c4f7fea +missing /local-remove + +# Now test with -F and -A forcing all installs. (-A should have +# precedence over -F) + +build_trees + +$COMMAND -A '/*' -rF -d $WORKDIR -D $TEST > $WORKDIR/testAF.out + +cat > $WORKDIR/correctAF.out <<EOF + D /remove + U /already + U /conflict + U /local + U /local-already + A /local-remove + U /old + U /add +EOF + +echo "Differences for -A '/*' -F:" +diff -u -L "correct" $WORKDIR/correctAF.out -L "test" $WORKDIR/testAF.out \ + || FAILED=yes + +missing /remove +file /old "" 6a9f34f109d94406a4de3bc5d72de259 +noconflict /old +file /already "" 21f4eca3aacc702c49878c8da7afd3d0 +noconflict /already +file /add "" 0208bd647111fedf6318511712ab9e97 +noconflict /add +file /conflict "" 75ee141c4136beaf14e39de92efa84e4 +noconflict /conflict +file /local "" 6a8fc5c2755b7a49015089f5e1dbe092 +file /local-already "" 49045f8b51542dd634655301cd296f66 +file /local-remove "" 5c38322efed4014797d7127f5c652d9d + +[ "${FAILED}" = no ] diff --git a/usr.sbin/etcupdate/tests/ignore_test.sh b/usr.sbin/etcupdate/tests/ignore_test.sh new file mode 100644 index 000000000000..20d1f9abcd54 --- /dev/null +++ b/usr.sbin/etcupdate/tests/ignore_test.sh @@ -0,0 +1,275 @@ +#!/bin/sh +# +# Copyright (c) 2010 Hudson River Trading LLC +# Written by: John H. Baldwin <jhb@FreeBSD.org> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# + +# Various regression tests to test the -I flag to the 'update' command. + +FAILED=no +WORKDIR=work + +usage() +{ + echo "Usage: ignore.sh [-s script] [-w workdir]" + exit 1 +} + +# Allow the user to specify an alternate work directory or script. +COMMAND=etcupdate +while getopts "s:w:" option; do + case $option in + s) + COMMAND="sh $OPTARG" + ;; + w) + WORKDIR=$OPTARG + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) +if [ $# -ne 0 ]; then + usage +fi + +CONFLICTS=$WORKDIR/conflicts +OLD=$WORKDIR/old +NEW=$WORKDIR/current +TEST=$WORKDIR/test + +# These tests deal with ignoring certain patterns of files. We run the +# test multiple times ignoring different patterns. +build_trees() +{ + local i + + rm -rf $OLD $NEW $TEST $CONFLICTS + mkdir -p $OLD $NEW $TEST + + for i in $OLD $NEW $TEST; do + mkdir -p $i/tree + done + + # tree: Test three different cases (add, modify, remove) that all + # match the tree/* glob. + echo "foo" > $NEW/tree/add + for i in $OLD $TEST; do + echo "old" > $i/tree/modify + done + echo "new" > $NEW/tree/modify + for i in $OLD $TEST; do + echo "old" > $i/tree/remove + done + + # rmdir: Remove a whole tree. + for i in $OLD $TEST; do + mkdir $i/rmdir + echo "foo" > $i/rmdir/file + done +} + +# $1 - relative path to file that should be missing from TEST +missing() +{ + if [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be missing" + FAILED=yes + fi +} + +# $1 - relative path to file that should be present in TEST +present() +{ + if ! [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be present" + FAILED=yes + fi +} + +# $1 - relative path to file that should be a directory in TEST +dir() +{ + if ! [ -d $TEST/$1 ]; then + echo "File $1 should be a directory" + FAILED=yes + fi +} + +# $1 - relative path to regular file that should be present in TEST +# $2 - optional string that should match file contents +# $3 - optional MD5 of the flie contents, overrides $2 if present +file() +{ + local contents sum + + if ! [ -f $TEST/$1 ]; then + echo "File $1 should be a regular file" + FAILED=yes + elif [ $# -eq 2 ]; then + contents=`cat $TEST/$1` + if [ "$contents" != "$2" ]; then + echo "File $1 has wrong contents" + FAILED=yes + fi + elif [ $# -eq 3 ]; then + sum=`md5 -q $TEST/$1` + if [ "$sum" != "$3" ]; then + echo "File $1 has wrong contents" + FAILED=yes + fi + fi +} + +# $1 - relative path to a regular file that should have a conflict +# $2 - optional MD5 of the conflict file contents +conflict() +{ + local sum + + if ! [ -f $CONFLICTS/$1 ]; then + echo "File $1 missing conflict" + FAILED=yes + elif [ $# -gt 1 ]; then + sum=`md5 -q $CONFLICTS/$1` + if [ "$sum" != "$2" ]; then + echo "Conflict $1 has wrong contents" + FAILED=yes + fi + fi +} + +# $1 - relative path to a regular file that should not have a conflict +noconflict() +{ + if [ -f $CONFLICTS/$1 ]; then + echo "File $1 should not have a conflict" + FAILED=yes + fi +} + +if [ `id -u` -ne 0 ]; then + echo "must be root" + exit 0 +fi + +if [ -r /etc/etcupdate.conf ]; then + echo "WARNING: /etc/etcupdate.conf settings may break some tests." +fi + +# First run the test ignoring no patterns. + +build_trees + +$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out + +cat > $WORKDIR/correct.out <<EOF + D /rmdir/file + D /tree/remove + D /rmdir + U /tree/modify + A /tree/add +EOF + +echo "Differences for regular:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \ + || FAILED=yes + +missing /tree/remove +file /tree/modify "new" +file /tree/add "foo" +missing /rmdir/file +missing /rmdir + +# Now test with -I '/tree/*'. This should preserve the /tree files. + +build_trees + +$COMMAND -r -I '/tree/*' -d $WORKDIR -D $TEST > $WORKDIR/test1.out + +cat > $WORKDIR/correct1.out <<EOF + D /rmdir/file + D /rmdir +EOF + +echo "Differences for -I '/tree/*':" +diff -u -L "correct" $WORKDIR/correct1.out -L "test" $WORKDIR/test1.out \ + || FAILED=yes + +file /tree/remove "old" +file /tree/modify "old" +missing /tree/add +missing /rmdir/file +missing /rmdir + +# Now test with two patterns. This should preserve everything. + +build_trees + +$COMMAND -r -I '/tree/*' -I '/rmdir*' -d $WORKDIR -D $TEST > \ + $WORKDIR/test2.out + +cat > $WORKDIR/correct2.out <<EOF +EOF + +echo "Differences for -I '/tree/*' -I '/rmdir*':" + +diff -u -L "correct" $WORKDIR/correct2.out -L "test" $WORKDIR/test2.out \ + || FAILED=yes + +file /tree/remove "old" +file /tree/modify "old" +missing /tree/add +file /rmdir/file "foo" + +# Now test with a pattern that should cause a warning on /rmdir by +# only ignoring the files under that directory. Note that this also +# tests putting two patterns into a single -I argument. + +build_trees + +$COMMAND -r -I '/tree/* /rmdir/*' -d $WORKDIR -D $TEST > \ + $WORKDIR/test3.out + +cat > $WORKDIR/correct3.out <<EOF +Warnings: + Non-empty directory remains: /rmdir +EOF + +echo "Differences for -I '/tree/* /rmdir/*':" + +diff -u -L "correct" $WORKDIR/correct3.out -L "test" $WORKDIR/test3.out \ + || FAILED=yes + +file /tree/remove "old" +file /tree/modify "old" +missing /tree/add +file /rmdir/file "foo" +dir /rmdir + +[ "${FAILED}" = no ] diff --git a/usr.sbin/etcupdate/tests/preworld_test.sh b/usr.sbin/etcupdate/tests/preworld_test.sh new file mode 100644 index 000000000000..e9a7544197a9 --- /dev/null +++ b/usr.sbin/etcupdate/tests/preworld_test.sh @@ -0,0 +1,251 @@ +#!/bin/sh +# +# Copyright (c) 2013 Hudson River Trading LLC +# Written by: John H. Baldwin <jhb@FreeBSD.org> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# + +# Regression tests for the pre-world (-p) mode + +FAILED=no +WORKDIR=work + +usage() +{ + echo "Usage: preworld.sh [-s script] [-w workdir]" + exit 1 +} + +# Allow the user to specify an alternate work directory or script. +COMMAND=etcupdate +while getopts "s:w:" option; do + case $option in + s) + COMMAND="sh $OPTARG" + ;; + w) + WORKDIR=$OPTARG + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) +if [ $# -ne 0 ]; then + usage +fi + +CONFLICTS=$WORKDIR/conflicts +SRC=$WORKDIR/src +OLD=$WORKDIR/current +TEST=$WORKDIR/test + +build_trees() +{ + + # Populate trees with pre-world files and additional files + # that should not be touched. + + rm -rf $SRC $OLD $TEST $CONFLICTS + + # Create the "old" source tree as the starting point + mkdir -p $OLD/etc + cat >> $OLD/etc/master.passwd <<EOF +# +root::0:0::0:0:Charlie &:/root:/bin/csh +toor:*:0:0::0:0:Bourne-again Superuser:/root: +daemon:*:1:1::0:0:Owner of many system processes:/root:/usr/sbin/nologin +operator:*:2:5::0:0:System &:/:/usr/sbin/nologin +_dhcp:*:65:65::0:0:dhcp programs:/var/empty:/usr/sbin/nologin +uucp:*:66:66::0:0:UUCP pseudo-user:/var/spool/uucppublic:/usr/local/libexec/uucp/uucico +pop:*:68:6::0:0:Post Office Owner:/nonexistent:/usr/sbin/nologin +www:*:80:80::0:0:World Wide Web Owner:/nonexistent:/usr/sbin/nologin +hast:*:845:845::0:0:HAST unprivileged user:/var/empty:/usr/sbin/nologin +nobody:*:65534:65534::0:0:Unprivileged user:/nonexistent:/usr/sbin/nologin +EOF + cat >> $OLD/etc/group <<EOF +# +wheel:*:0:root +daemon:*:1: +kmem:*:2: +sys:*:3: +tty:*:4: +operator:*:5:root +_dhcp:*:65: +uucp:*:66: +dialer:*:68: +network:*:69: +www:*:80: +hast:*:845: +nogroup:*:65533: +nobody:*:65534: +EOF + cat >> $OLD/etc/inetd.conf <<EOF +# Yet another file +EOF + + # Copy the "old" source tree to the test tree and make local + # modifications. + cp -R $OLD $TEST + sed -I "" -e 's/root::/root:<rpass>:/' $TEST/etc/master.passwd + cat >> $TEST/etc/master.passwd <<EOF +john:<password>:1001:1001::0:0:John Baldwin:/home/john:/bin/tcsh +messagebus:*:556:556::0:0:D-BUS Daemon User:/nonexistent:/usr/sbin/nologin +polkit:*:562:562::0:0:PolicyKit User:/nonexistent:/usr/sbin/nologin +haldaemon:*:560:560::0:0:HAL Daemon User:/nonexistent:/usr/sbin/nologin +EOF + awk '/wheel/ { printf "%s,john\n", $0; next } // { print }' \ + $OLD/etc/group > $TEST/etc/group + cat >> $TEST/etc/group <<EOF +john:*:1001: +messagebus:*:556: +polkit:*:562: +haldaemon:*:560: +EOF + rm $TEST/etc/inetd.conf + touch $TEST/etc/localtime + + # Copy the "old" source tree to the new source tree and + # make upstream modifications. + cp -R $OLD $SRC + sed -I "" -e '/:80:/i\ +auditdistd:*:78:77::0:0:Auditdistd unprivileged user:/var/empty:/usr/sbin/nologin' \ + $SRC/etc/master.passwd + sed -I "" -e '/:80:/i\ +audit:*:77:' \ + $SRC/etc/group + cat >> $SRC/etc/inetd.conf <<EOF +# Making this larger +EOF +} + +# $1 - relative path to file that should be missing from TEST +missing() +{ + if [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be missing" + FAILED=yes + fi +} + +# $1 - relative path to file that should be present in TEST +present() +{ + if ! [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be present" + FAILED=yes + fi +} + +# $1 - relative path to regular file that should be present in TEST +# $2 - optional string that should match file contents +# $3 - optional MD5 of the flie contents, overrides $2 if present +file() +{ + local contents sum + + if ! [ -f $TEST/$1 ]; then + echo "File $1 should be a regular file" + FAILED=yes + elif [ $# -eq 2 ]; then + contents=`cat $TEST/$1` + if [ "$contents" != "$2" ]; then + echo "File $1 has wrong contents" + FAILED=yes + fi + elif [ $# -eq 3 ]; then + sum=`md5 -q $TEST/$1` + if [ "$sum" != "$3" ]; then + echo "File $1 has wrong contents" + FAILED=yes + fi + fi +} + +# $1 - relative path to a regular file that should have a conflict +# $2 - optional MD5 of the conflict file contents +conflict() +{ + local sum + + if ! [ -f $CONFLICTS/$1 ]; then + echo "File $1 missing conflict" + FAILED=yes + elif [ $# -gt 1 ]; then + sum=`md5 -q $CONFLICTS/$1` + if [ "$sum" != "$2" ]; then + echo "Conflict $1 has wrong contents" + FAILED=yes + fi + fi +} + +check_trees() +{ + + echo "Checking tree for correct results:" + + file /etc/master.passwd "" 1385366e8b424d33d59b7d8a2bdb15d3 + file /etc/group "" 21273f845f6ec0cda9188c4ddac9ed47 + missing /etc/inetd.conf + + # These should be auto-generated by pwd_mkdb + file /etc/passwd "" e4650d2727044b22d513e6a02d86bcfa + file /etc/pwd.db + file /etc/spwd.db +} + +if [ `id -u` -ne 0 ]; then + echo "must be root" + exit 0 +fi + +if [ -r /etc/etcupdate.conf ]; then + echo "WARNING: /etc/etcupdate.conf settings may break some tests." +fi + +build_trees + +$COMMAND -np -s $SRC -d $WORKDIR -D $TEST > $WORKDIR/testn.out + +cat > $WORKDIR/correct.out <<EOF + M /etc/group + M /etc/master.passwd +EOF + +echo "Differences for -n:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/testn.out \ + || FAILED=yes + +$COMMAND -p -s $SRC -d $WORKDIR -D $TEST > $WORKDIR/test.out + +echo "Differences for real:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \ + || FAILED=yes + +check_trees + +[ "${FAILED}" = no ] diff --git a/usr.sbin/etcupdate/tests/tests_test.sh b/usr.sbin/etcupdate/tests/tests_test.sh new file mode 100644 index 000000000000..794be8065a96 --- /dev/null +++ b/usr.sbin/etcupdate/tests/tests_test.sh @@ -0,0 +1,1020 @@ +#!/bin/sh +# +# Copyright (c) 2010 Hudson River Trading LLC +# Written by: John H. Baldwin <jhb@FreeBSD.org> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# + +# Various regression tests to run for the 'update' command. + +FAILED=no +WORKDIR=work + +usage() +{ + echo "Usage: tests.sh [-s script] [-w workdir]" + exit 1 +} + +# Allow the user to specify an alternate work directory or script. +COMMAND=etcupdate +while getopts "s:w:" option; do + case $option in + s) + COMMAND="sh $OPTARG" + ;; + w) + WORKDIR=$OPTARG + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) +if [ $# -ne 0 ]; then + usage +fi + +CONFLICTS=$WORKDIR/conflicts +OLD=$WORKDIR/old +NEW=$WORKDIR/current +TEST=$WORKDIR/test + +# The various states of the comparison of a file between two trees. +states="equal first second difftype difflinks difffiles" + +build_trees() +{ + local i j k + + rm -rf $OLD $NEW $TEST $CONFLICTS + mkdir -p $OLD/etc $NEW/etc $TEST/etc + + # For an given file, there are three different pair-wise + # relations between the three threes (old, new, and test): old + # vs new, old vs test, and new vs test. Each of these + # relations takes on one of six different states from the + # 'compare()' function in etcupdate: equal, onlyfirst, + # onlysecond, difftype, difflinks, difffiles. In addition, + # there are special considerations for considering cases such + # as a file merge that results in conflicts versus one that + # does not, special treatment of directories, etc. The tests + # below attempt to enumerate the three dimensional test matrix + # by having the path name use the three different tree states + # for the parent directories. + # + # Note that if the old and new files are identical (so first + # compare is "equal"), then the second and third comparisons + # will be the same. + # + # Note also that etcupdate only cares about files that are + # present in at least one of the old or new trees. Thus, none + # of the '*/second/second' cases are relevant. + + for i in $states; do + for j in $states; do + for k in $states; do + mkdir -p $OLD/$i/$j/$k $NEW/$i/$j/$k \ + $TEST/$i/$j/$k + done + done + done + + # /equal/equal/equal: Everything is equal. Nothing should happen. + for i in $OLD $NEW $TEST; do + mkfifo $i/equal/equal/equal/fifo + echo "foo" > $i/equal/equal/equal/file + mkdir $i/equal/equal/equal/dir + ln -s "bar" $i/equal/equal/equal/link + done + + # /equal/first/first: The file is missing from the test + # directory. Nothing should happen. + for i in $OLD $NEW; do + mkfifo $i/equal/first/first/fifo + echo "foo" > $i/equal/first/first/file + mkdir $i/equal/first/first/dir + ln -s "bar" $i/equal/first/first/link + done + + # /equal/difftype/difftype: The local file is a different + # type. Nothing should happen. + for i in $OLD $NEW; do + mkfifo $i/equal/difftype/difftype/fifo + mkdir $i/equal/difftype/difftype/fromdir + done + echo "bar" > $TEST/equal/difftype/difftype/fifo + ln -s "test" $TEST/equal/difftype/difftype/fromdir + + # /equal/difflinks/difflinks: The local file is a modified + # link. Nothing should happen. + for i in $OLD $NEW; do + ln -s "foo" $i/equal/difflinks/difflinks/link + done + ln -s "bar" $TEST/equal/difflinks/difflinks/link + + # /equal/difffiles/difffiles: The local file is a modified + # file. Nothing should happen. + for i in $OLD $NEW; do + echo "foo" > $i/equal/difffiles/difffiles/file + done + echo "bar" > $TEST/equal/difffiles/difffiles/file + + # /first/equal/second: Remove unmodified files. The files + # should all be removed. + for i in $OLD $TEST; do + mkfifo $i/first/equal/second/fifo + echo "foo" > $i/first/equal/second/file + mkdir $i/first/equal/second/emptydir + ln -s "bar" $i/first/equal/second/link + mkdir $i/first/equal/second/fulldir + echo "foo" > $i/first/equal/second/fulldir/file + done + + # /first/equal/*: Cannot occur. If the file is missing from + # new, then new vs test will always be 'second'. + + # /first/first/equal: Removed files are already removed. + # Nothing should happen. + mkfifo $OLD/first/first/equal/fifo + echo "foo" > $OLD/first/first/equal/file + mkdir $OLD/first/first/equal/dir + ln -s "bar" $OLD/first/first/equal/link + + # /first/first/*: Cannot occur. The files are missing from + # both new and test. + + # /first/second/*: Cannot happen, if the file is in old for + # old vs new, it cannot be missing for old vs test. + + # /first/difftype/second: File with different local type + # removed. Should generate a warning. + mkfifo $OLD/first/difftype/second/fifo + mkdir $TEST/first/difftype/second/fifo + + # /first/difftype/*: Cannot happen since the file is missing + # from new but present in test. + + # /first/difflinks/second: Modified link removed. Should + # generate a warning. + ln -s "old link" $OLD/first/difflinks/second/link + ln -s "test link" $TEST/first/difflinks/second/link + + # /first/difflinks/*: Cannot happen since the file is missing + # from new but present in test. + + # /first/difffiles/second: Modified file removed. Should + # generate a warning. + echo "foo" > $OLD/first/difffiles/second/file + echo "bar" > $TEST/first/difffiles/second/file + + # /first/difffiles/*: Cannot happen since the file is missing + # from new but present in test. + + # /second/equal/first: Added a new file that isn't present in + # test. The empty directory should be ignored. + echo "bar" > $NEW/second/equal/first/file + mkfifo $NEW/second/equal/first/fifo + ln -s "new" $NEW/second/equal/first/link + mkdir $NEW/second/equal/first/emptydir + mkdir $NEW/second/equal/first/fulldir + echo "foo" > $NEW/second/equal/first/fulldir/file + + # /second/equal/*: Cannot happen since the file is missing + # from test but present in new. + + # /second/first/*: Cannot happen since the file is missing + # from old. + + # /second/second/equal: Newly added file is already present in + # the test directory and identical to the new file. Nothing + # should happen. + for i in $NEW $TEST; do + mkfifo $i/second/second/equal/fifo + echo "foo" > $i/second/second/equal/file + mkdir $i/second/second/equal/dir + ln -s "bar" $i/second/second/equal/link + done + + # /second/second/first: Cannot happen. The file is in dest in + # the second test, so it can't be missing from the third test. + + # /second/second/second: Cannot happen. The file is in new in + # the first test, so it can't be missing from the third test. + + # /second/second/difftype: Newly added file conflicts with + # existing file in test tree of a different type. Should + # generate a warning. + mkdir $NEW/second/second/difftype/dir + mkfifo $TEST/second/second/difftype/dir + + # /second/second/difflinks: Newly added link conflicts with + # existing link in test tree. Should generate a warning. + ln -s "new link" $NEW/second/second/difflinks/link + ln -s "test link" $TEST/second/second/difflinks/link + + # /second/second/difffiles: Newly added file conflicts with + # existing file in test tree. Should generate a warning. + echo "new" > $NEW/second/second/difffiles/file + echo "test" > $TEST/second/second/difffiles/file + + # /second/difftype/*: Cannot happen since the file is missing + # from old. + + # /second/difflinks/*: Cannot happen since the file is missing + # from old. + + # /second/difffiles/*: Cannot happen since the file is missing + # from old. + + # /difftype/equal/difftype: Unmodified file has changed type. + # File should be updated to the new file. In the 'todir' case + # the directory won't actually be created because it is empty. + for i in $OLD $TEST; do + echo "foo" > $i/difftype/equal/difftype/file + mkdir $i/difftype/equal/difftype/fromdir + ln -s "old" $i/difftype/equal/difftype/todir + done + ln -s "test" $NEW/difftype/equal/difftype/file + mkfifo $NEW/difftype/equal/difftype/fromdir + mkdir $NEW/difftype/equal/difftype/todir + + # /difftype/equal/*: Cannot happen. Since the old file is a + # difftype from the new file and the test file is identical to + # the old file, the test file must be a difftype from the new + # file. + + # /difftype/first/first: A removed file has changed type. + # This should generate a warning. + mkfifo $OLD/difftype/first/first/fifo + mkdir $NEW/difftype/first/first/fifo + + # /difftype/first/*: Cannot happen. Since the new file exists + # and the dest file is missing, the last test must be 'first'. + + # /difftype/second/*: Cannot happen. The old file exists in + # the first test, so it cannot be missing in the second test. + + # /difftype/difftype/equal: A file has changed type, but the + # file in the test directory already matches the new file. Do + # nothing. + echo "foo" > $OLD/difftype/difftype/equal/fifo + mkfifo $OLD/difftype/difftype/equal/file + for i in $NEW $TEST; do + mkfifo $i/difftype/difftype/equal/fifo + echo "bar" > $i/difftype/difftype/equal/file + done + + # /difftype/difftype/first: Cannot happen. The dest file + # exists in the second test. + + # /difftype/difftype/second: Cannot happen. The new file + # exists in the first test. + + # /difftype/difftype/difftype: All three files (old, new, and + # test) are different types from each other. This should + # generate a warning. + mkfifo $OLD/difftype/difftype/difftype/one + mkdir $NEW/difftype/difftype/difftype/one + echo "foo" > $TEST/difftype/difftype/difftype/one + mkdir $OLD/difftype/difftype/difftype/two + echo "baz" > $NEW/difftype/difftype/difftype/two + ln -s "bar" $TEST/difftype/difftype/difftype/two + + # /difftype/difftype/difflinks: A file has changed from a + # non-link to a link in both the new and test trees, but the + # target of the new and test links differ. This should + # generate a new link conflict. + mkfifo $OLD/difftype/difftype/difflinks/link + ln -s "new" $NEW/difftype/difftype/difflinks/link + ln -s "test" $TEST/difftype/difftype/difflinks/link + + # /difftype/difftype/difffile: A file has changed from a + # non-regular file to a regular file in both the new and test + # trees, but the contents in the new and test files differ. + # This should generate a new file conflict. + ln -s "old" $OLD/difftype/difftype/difffiles/file + echo "foo" > $NEW/difftype/difftype/difffiles/file + echo "bar" > $TEST/difftype/difftype/difffiles/file + + # /difflinks/equal/difflinks: An unmodified symlink has + # changed. The link should be updated. + for i in $OLD $TEST; do + ln -s "old" $i/difflinks/equal/difflinks/link + done + ln -s "new" $NEW/difflinks/equal/difflinks/link + + # /difflinks/equal/*: Cannot happen. Since old is identical + # to test, the third test must be 'difflinks'. + + # /difflinks/first/first: A modified link is missing in the + # test tree. This should generate a warning. + ln -s "old" $OLD/difflinks/first/first/link + ln -s "new" $NEW/difflinks/first/first/link + + # /difflinks/first/*: Cannot happen. Since the test file is + # missing in the second test, it must be missing in the third + # test. + + # /difflinks/second/*: Cannot happen. The old link is present + # in the first test, so it cannot be missing in the second + # test. + + # /difflinks/difftype/difftype: An updated link has been + # changed to a different file type in the test tree. This + # should generate a warning. + ln -s "old" $OLD/difflinks/difftype/difftype/link + ln -s "new" $NEW/difflinks/difftype/difftype/link + echo "test" > $TEST/difflinks/difftype/difftype/link + + # /difflinks/difftype/*: Cannot happen. The old and new files + # are both links and the test file is not a link, so the third + # test must be 'difftype'. + + # /difflinks/difflinks/equal: An updated link has already been + # updated to the new target in the test tree. Nothing should + # happen. + ln -s "old" $OLD/difflinks/difflinks/equal/link + for i in $NEW $TEST; do + ln -s "new" $i/difflinks/difflinks/equal/link + done + + # /difflinks/difflinks/difflinks: An updated link has been + # modified in the test tree and doesn't match either the old + # or new links. This should generate a warning. + ln -s "old" $OLD/difflinks/difflinks/difflinks/link + ln -s "new" $NEW/difflinks/difflinks/difflinks/link + ln -s "test" $TEST/difflinks/difflinks/difflinks/link + + # /difflinks/difflinks/*: Cannot happen. All three files are + # links from the first two tests, so the third test can only + # be 'equal' or 'difflink'. + + # /difflinks/difffiles/*: Cannot happen. The old file is a + # link in the first test, so it cannot be a regular file in + # the second. + + # /difffiles/equal/difffiles: An unmodified file has been + # changed in new tree. The file should be updated to the new + # version. + for i in $OLD $TEST; do + echo "foo" > $i/difffiles/equal/difffiles/file + done + echo "bar" > $NEW/difffiles/equal/difffiles/file + + # /difffiles/equal/*: Cannot happen. Since the old file is + # identical to the test file, the third test must be + # 'difffiles'. + + # /difffiles/first/first: A removed file has been changed in + # the new tree. This should generate a warning. + echo "foo" > $OLD/difffiles/first/first/file + echo "bar" > $NEW/difffiles/first/first/file + + # /difffiles/first/*: Cannot happen. The new file is a + # regular file from the first test and the test file is + # missing in the second test, so the third test must be + # 'first'. + + # /difffiles/second/*: Cannot happen. The old file is present + # in the first test, so it must be present in the second test. + + # /difffiles/difftype/difftype: An updated regular file has + # been changed to a different file type in the test tree. + # This should generate a warning. + echo "old" > $OLD/difffiles/difftype/difftype/file + echo "new" > $NEW/difffiles/difftype/difftype/file + mkfifo $TEST/difffiles/difftype/difftype/file + + # /difffiles/difftype/*: Cannot happen. The new file is known + # to be a regular file from the first test, and the test file + # is known to exist as a different file type from the second + # test. The third test must be 'difftype'. + + # /difffiles/difflink/*: Cannot happen. The old file is known + # to be a regular file from the first test, so it cannot be a + # link in the second test. + + # /difffiles/difffiles/equal: An updated regular file has + # already been updated to match the new file in the test tree. + # Nothing should happen. + echo "foo" > $OLD/difffiles/difffiles/equal/file + for i in $NEW $TEST; do + echo "bar" > $i/difffiles/difffiles/equal/file + done + + # /difffiles/difffiles/difffiles: A modified regular file was + # updated in the new tree. The changes should be merged into + # to the new file if possible. If the merge fails, a conflict + # should be generated. + cat > $OLD/difffiles/difffiles/difffiles/simple <<EOF +this is an old line + +EOF + cat > $NEW/difffiles/difffiles/difffiles/simple <<EOF +this is a new line + +EOF + cat > $TEST/difffiles/difffiles/difffiles/simple <<EOF +this is an old line + +this is a local line +EOF + cat > $OLD/difffiles/difffiles/difffiles/conflict <<EOF +this is an old file +EOF + cat > $NEW/difffiles/difffiles/difffiles/conflict <<EOF +this is a new file +EOF + cat > $TEST/difffiles/difffiles/difffiles/conflict <<EOF +this is a test file +EOF + + # /difffiles/difffiles/*: Cannot happen. From the first three + # tests, all three files are regular files. The test file can + # either be identical to the new file ('equal') or not + # ('difffiles'). + + ## Tests for adding directories + mkdir -p $OLD/adddir $NEW/adddir $TEST/adddir + + # /adddir/conflict: Add a new file in a directory that already + # exists as a file. This should generate two warnings. + mkdir $NEW/adddir/conflict + touch $NEW/adddir/conflict/newfile + touch $TEST/adddir/conflict + + # /adddir/partial: Add a new file in a directory. The + # directory already exists in the test tree and contains a + # different local file. The new file from the new tree should + # be added. + for i in $NEW $TEST; do + mkdir $i/adddir/partial + done + echo "foo" > $NEW/adddir/partial/file + mkfifo $TEST/adddir/partial/fifo + + ## Tests for removing directories + mkdir -p $OLD/rmdir $NEW/rmdir $TEST/rmdir + + # /rmdir/extra: Do not remove a directory with an extra local file. + # This should generate a warning. + for i in $OLD $TEST; do + mkdir $i/rmdir/extra + done + echo "foo" > $TEST/rmdir/extra/localfile.txt + + # /rmdir/conflict: Do not remove a directory with a conflicted + # remove file. This should generate a warning. + for i in $OLD $TEST; do + mkdir $i/rmdir/conflict + done + mkfifo $OLD/rmdir/conflict/difftype + mkdir $TEST/rmdir/conflict/difftype + + # /rmdir/partial: Remove a complete hierarchy when part of the + # tree has already been removed locally. + for i in $OLD $TEST; do + mkdir -p $i/rmdir/partial/subdir + mkfifo $i/rmdir/partial/subdir/fifo + done + echo "foo" > $OLD/rmdir/partial/subdir/file + + ## Tests for converting files to directories and vice versa + for i in $OLD $NEW $TEST; do + for j in already old fromdir todir; do + mkdir -p $i/dirchange/$j + done + done + + # /dirchange/already/fromdir: Convert a directory tree to a + # file without conflicts where the test tree already has the + # new file. Nothing should happen. + mkdir $OLD/dirchange/already/fromdir + echo "blah" > $OLD/dirchange/already/fromdir/somefile + for i in $NEW $TEST; do + echo "bar" > $i/dirchange/already/fromdir + done + + # /dirchange/already/todir: Convert an unmodified file to a + # directory tree where the test tree already has the new + # tree. Nothing should happen. + echo "baz" > $OLD/dirchange/already/todir + for i in $NEW $TEST; do + mkdir $i/dirchange/already/todir + echo "blah" > $i/dirchange/already/todir/somefile + done + + # /dirchange/old/fromdir: Convert a directory tree to a file. + # The old files are unmodified and should be changed to the new tree. + for i in $OLD $TEST; do + mkdir $i/dirchange/old/fromdir + echo "blah" > $i/dirchange/old/fromdir/somefile + done + echo "bar" > $NEW/dirchange/old/fromdir + + # /dirchange/old/todir: Convert a file to a directory tree. + # The old file is unmodified and should be changed to the new + # tree. + for i in $OLD $TEST; do + echo "foo" > $i/dirchange/old/todir + done + mkdir $NEW/dirchange/old/todir + echo "bar" > $NEW/dirchange/old/todir/file + + # /dirchange/fromdir/extradir: Convert a directory tree to a + # file. The test tree includes an extra file in the directory + # that is not present in the old tree. This should generate a + # warning. + for i in $OLD $TEST; do + mkdir $i/dirchange/fromdir/extradir + echo "foo" > $i/dirchange/fromdir/extradir/file + done + mkfifo $TEST/dirchange/fromdir/extradir/fifo + ln -s "bar" $NEW/dirchange/fromdir/extradir + + # /dirchange/fromdir/conflict: Convert a directory tree to a + # file. The test tree includes a local change that generates + # a warning and prevents the removal of the directory. + for i in $OLD $TEST; do + mkdir $i/dirchange/fromdir/conflict + done + echo "foo" > $OLD/dirchange/fromdir/conflict/somefile + echo "bar" > $TEST/dirchange/fromdir/conflict/somefile + mkfifo $NEW/dirchange/fromdir/conflict + + # /dirchange/todir/difffile: Convert a file to a directory + # tree. The test tree has a locally modified version of the + # file so that the conversion fails with a warning. + echo "foo" > $OLD/dirchange/todir/difffile + mkdir $NEW/dirchange/todir/difffile + echo "baz" > $NEW/dirchange/todir/difffile/file + echo "bar" > $TEST/dirchange/todir/difffile + + # /dirchange/todir/difftype: Similar to the previous test, but + # the conflict is due to a change in the file type. + echo "foo" > $OLD/dirchange/todir/difftype + mkdir $NEW/dirchange/todir/difftype + echo "baz" > $NEW/dirchange/todir/difftype/file + mkfifo $TEST/dirchange/todir/difftype + + ## Tests for post-install actions + + # - Adding /etc/master.passwd should cause pwd_mkdb to be run + echo "foo:*:16000:100::0:0:& user:/home/foo:/bin/tcsh" > \ + $NEW/etc/master.passwd + + # - Verify that updating an unmodified /etc/login.conf builds + # /etc/login.conf.db. + cat > $OLD/etc/login.conf <<EOF +default:\\ + :passwd_format=md5: +EOF + cat > $NEW/etc/login.conf <<EOF +default:\\ + :passwd_format=md5:\\ + :copyright=/etc/COPYRIGHT +EOF + cp $OLD/etc/login.conf $TEST/etc/login.conf + + # - Verify that a merge without conflicts to /etc/mail/aliases + # will trigger a newaliases run request. + mkdir -p $OLD/etc/mail $NEW/etc/mail $TEST/etc/mail + cat > $OLD/etc/mail/aliases <<EOF +# root: me@my.domain + +# Basic system aliases -- these MUST be present +MAILER-DAEMON: postmaster +postmaster: root +EOF + cat > $NEW/etc/mail/aliases <<EOF +# root: me@my.domain + +# Basic system aliases -- these MUST be present +MAILER-DAEMON: postmaster +postmaster: root + +# General redirections for pseudo accounts +_dhcp: root +_pflogd: root +EOF + cat > $TEST/etc/mail/aliases <<EOF +root: someone@example.com + +# Basic system aliases -- these MUST be present +MAILER-DAEMON: postmaster +postmaster: root +EOF + + # - Verify that updating an unmodified /etc/services builds + # /var/db/services.db. + cat > $OLD/etc/services <<EOF +rtmp 1/ddp #Routing Table Maintenance Protocol +tcpmux 1/tcp #TCP Port Service Multiplexer +tcpmux 1/udp #TCP Port Service Multiplexer +EOF + cat > $NEW/etc/services <<EOF +rtmp 1/ddp #Routing Table Maintenance Protocol +tcpmux 1/tcp #TCP Port Service Multiplexer +tcpmux 1/udp #TCP Port Service Multiplexer +nbp 2/ddp #Name Binding Protocol +compressnet 2/tcp #Management Utility +compressnet 2/udp #Management Utility +EOF + cp $OLD/etc/services $TEST/etc/services + mkdir -p $TEST/var/db +} + +# $1 - relative path to file that should be missing from TEST +missing() +{ + if [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be missing" + FAILED=yes + fi +} + +# $1 - relative path to file that should be present in TEST +present() +{ + if ! [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be present" + FAILED=yes + fi +} + +# $1 - relative path to file that should be a fifo in TEST +fifo() +{ + if ! [ -p $TEST/$1 ]; then + echo "File $1 should be a FIFO" + FAILED=yes + fi +} + +# $1 - relative path to file that should be a directory in TEST +dir() +{ + if ! [ -d $TEST/$1 ]; then + echo "File $1 should be a directory" + FAILED=yes + fi +} + +# $1 - relative path to file that should be a symlink in TEST +# $2 - optional value of the link +link() +{ + local val + + if ! [ -L $TEST/$1 ]; then + echo "File $1 should be a link" + FAILED=yes + elif [ $# -gt 1 ]; then + val=`readlink $TEST/$1` + if [ "$val" != "$2" ]; then + echo "Link $1 should link to \"$2\"" + FAILED=yes + fi + fi +} + +# $1 - relative path to regular file that should be present in TEST +# $2 - optional string that should match file contents +# $3 - optional MD5 of the flie contents, overrides $2 if present +file() +{ + local contents sum + + if ! [ -f $TEST/$1 ]; then + echo "File $1 should be a regular file" + FAILED=yes + elif [ $# -eq 2 ]; then + contents=`cat $TEST/$1` + if [ "$contents" != "$2" ]; then + echo "File $1 has wrong contents" + FAILED=yes + fi + elif [ $# -eq 3 ]; then + sum=`md5 -q $TEST/$1` + if [ "$sum" != "$3" ]; then + echo "File $1 has wrong contents" + FAILED=yes + fi + fi +} + +# $1 - relative path to a regular file that should have a conflict +# $2 - optional MD5 of the conflict file contents +conflict() +{ + local sum + + if ! [ -f $CONFLICTS/$1 ]; then + echo "File $1 missing conflict" + FAILED=yes + elif [ $# -gt 1 ]; then + sum=`md5 -q $CONFLICTS/$1` + if [ "$sum" != "$2" ]; then + echo "Conflict $1 has wrong contents" + FAILED=yes + fi + fi +} + +check_trees() +{ + + echo "Checking tree for correct results:" + + ## /equal/equal/equal: + fifo /equal/equal/equal/fifo + file /equal/equal/equal/file "foo" + dir /equal/equal/equal/dir + link /equal/equal/equal/link "bar" + + ## /equal/first/first: + missing /equal/first/first/fifo + missing /equal/first/first/file + missing /equal/first/first/dir + missing /equal/first/first/link + + ## /equal/difftype/difftype: + file /equal/difftype/difftype/fifo "bar" + link /equal/difftype/difftype/fromdir "test" + + ## /equal/difflinks/difflinks: + link /equal/difflinks/difflinks/link "bar" + + ## /equal/difffiles/difffiles: + file /equal/difffiles/difffiles/file "bar" + + ## /first/equal/second: + missing /first/equal/second/fifo + missing /first/equal/second/file + missing /first/equal/second/emptydir + missing /first/equal/second/link + missing /first/equal/second/fulldir + + ## /first/first/equal: + missing /first/first/equal/fifo + missing /first/first/equal/file + missing /first/first/equal/dir + missing /first/first/equal/link + + ## /first/difftype/second: + present /first/difftype/second/fifo + + ## /first/difflinks/second: + link /first/difflinks/second/link "test link" + + ## /first/difffiles/second: + file /first/difffiles/second/file "bar" + + ## /second/equal/first: + file /second/equal/first/file "bar" + fifo /second/equal/first/fifo + link /second/equal/first/link "new" + missing /second/equal/first/emptydir + file /second/equal/first/fulldir/file "foo" + + ## /second/second/equal: + fifo /second/second/equal/fifo + file /second/second/equal/file "foo" + dir /second/second/equal/dir + link /second/second/equal/link "bar" + + ## /second/second/difftype: + fifo /second/second/difftype/dir + + ## /second/second/difflinks: + link /second/second/difflinks/link "test link" + + ## /second/second/difffiles: + file /second/second/difffiles/file "test" + conflict /second/second/difffiles/file 4f2ee8620a251fd53f06bb6112eb6ffa + + ## /difftype/equal/difftype: + link /difftype/equal/difftype/file "test" + fifo /difftype/equal/difftype/fromdir + missing /difftype/equal/difftype/todir + + ## /difftype/first/first: + missing /difftype/first/first/fifo + + ## /difftype/difftype/equal: + fifo /difftype/difftype/equal/fifo + file /difftype/difftype/equal/file "bar" + + ## /difftype/difftype/difftype: + file /difftype/difftype/difftype/one "foo" + link /difftype/difftype/difftype/two "bar" + + ## /difftype/difftype/difflinks: + link /difftype/difftype/difflinks/link "test" + + ## /difftype/difftype/difffile: + conflict /difftype/difftype/difffiles/file \ + 117f2bcd1f6491f6044e79e5a57a9229 + + ## /difflinks/equal/difflinks: + link /difflinks/equal/difflinks/link "new" + + ## /difflinks/first/first: + missing /difflinks/first/first/link + + ## /difflinks/difftype/difftype: + file /difflinks/difftype/difftype/link "test" + + ## /difflinks/difflinks/equal: + link /difflinks/difflinks/equal/link "new" + + ## /difflinks/difflinks/difflinks: + link /difflinks/difflinks/difflinks/link "test" + + ## /difffiles/equal/difffiles: + file /difffiles/equal/difffiles/file "bar" + + ## /difffiles/first/first: + missing /difffiles/first/first/file + + ## /difffiles/difftype/difftype: + fifo /difffiles/difftype/difftype/file + + ## /difffiles/difffiles/equal: + file /difffiles/difffiles/equal/file "bar" + + ## /difffiles/difffiles/difffiles: + file /difffiles/difffiles/difffiles/simple "" \ + cabc7e5e80b0946d79edd555e9648486 + file /difffiles/difffiles/difffiles/conflict "this is a test file" + conflict /difffiles/difffiles/difffiles/conflict \ + 8261cfdd89280c4a6c26e4ac86541fe9 + + ## /adddir/conflict: + file /adddir/conflict + + ## /adddir/partial: + file /adddir/partial/file "foo" + fifo /adddir/partial/fifo + + ## /rmdir/extra: + dir /rmdir/extra + file /rmdir/extra/localfile.txt "foo" + + ## /rmdir/conflict: + dir /rmdir/conflict/difftype + present /rmdir/conflict + + ## /rmdir/partial: + missing /rmdir/partial + + ## /dirchange/already/fromdir: + file /dirchange/already/fromdir "bar" + + ## /dirchange/already/todir: + file /dirchange/already/todir/somefile "blah" + + ## /dirchange/old/fromdir: + file /dirchange/old/fromdir "bar" + + ## /dirchange/old/todir + file /dirchange/old/todir/file "bar" + + ## /dirchange/fromdir/extradir: + missing /dirchange/fromdir/extradir/file + fifo /dirchange/fromdir/extradir/fifo + + ## /dirchange/fromdir/conflict: + file /dirchange/fromdir/conflict/somefile "bar" + + ## /dirchange/todir/difffile: + file /dirchange/todir/difffile "bar" + + ## /dirchange/todir/difftype: + fifo /dirchange/todir/difftype + + ## Tests for post-install actions + file /etc/master.passwd + file /etc/passwd + file /etc/pwd.db + file /etc/spwd.db + file /etc/login.conf "" 7774a0f9a3a372c7c109c32fd31c4b6b + file /etc/login.conf.db + file /etc/mail/aliases "" 7d598f89ec040ab56af54011bdb83337 + file /etc/services "" 37fb6a8d1273f3b78329d431f21d9c7d + file /var/db/services.db +} + +if [ `id -u` -ne 0 ]; then + echo "must be root" + exit 0 +fi + +if [ -r /etc/etcupdate.conf ]; then + echo "WARNING: /etc/etcupdate.conf settings may break some tests." +fi + +build_trees + +$COMMAND -nr -d $WORKDIR -D $TEST > $WORKDIR/testn.out + +cat > $WORKDIR/correct.out <<EOF + D /dirchange/fromdir/extradir/file + D /dirchange/old/fromdir/somefile + D /first/equal/second/fifo + D /first/equal/second/file + D /first/equal/second/fulldir/file + D /first/equal/second/link + D /rmdir/partial/subdir/fifo + D /rmdir/partial/subdir + D /rmdir/partial + D /first/equal/second/fulldir + D /first/equal/second/emptydir + C /difffiles/difffiles/difffiles/conflict + M /difffiles/difffiles/difffiles/simple + U /difffiles/equal/difffiles/file + U /difflinks/equal/difflinks/link + C /difftype/difftype/difffiles/file + U /difftype/equal/difftype/file + U /difftype/equal/difftype/fromdir + D /difftype/equal/difftype/todir + U /dirchange/old/fromdir + U /dirchange/old/todir + U /etc/login.conf + M /etc/mail/aliases + U /etc/services + A /adddir/partial/file + A /dirchange/old/todir/file + A /etc/master.passwd + A /second/equal/first/fifo + A /second/equal/first/file + A /second/equal/first/fulldir/file + A /second/equal/first/link + C /second/second/difffiles/file +Warnings: + Modified regular file remains: /dirchange/fromdir/conflict/somefile + Modified regular file remains: /first/difffiles/second/file + Modified symbolic link remains: /first/difflinks/second/link + Modified directory remains: /first/difftype/second/fifo + Modified directory remains: /rmdir/conflict/difftype + Non-empty directory remains: /rmdir/extra + Non-empty directory remains: /rmdir/conflict + Modified mismatch: /difffiles/difftype/difftype/file (regular file vs fifo file) + Removed file changed: /difffiles/first/first/file + Modified link changed: /difflinks/difflinks/difflinks/link ("old" became "new") + Modified mismatch: /difflinks/difftype/difftype/link (symbolic link vs regular file) + Removed link changed: /difflinks/first/first/link ("old" became "new") + New link conflict: /difftype/difftype/difflinks/link ("new" vs "test") + Modified regular file changed: /difftype/difftype/difftype/one (fifo file became directory) + Modified symbolic link changed: /difftype/difftype/difftype/two (directory became regular file) + Remove mismatch: /difftype/first/first/fifo (fifo file became directory) + Modified directory changed: /dirchange/fromdir/conflict (directory became fifo file) + Modified directory changed: /dirchange/fromdir/extradir (directory became symbolic link) + Modified regular file changed: /dirchange/todir/difffile (regular file became directory) + Modified fifo file changed: /dirchange/todir/difftype (regular file became directory) + New file mismatch: /adddir/conflict (directory vs regular file) + Directory mismatch: $TEST/adddir/conflict (regular file) + Directory mismatch: $TEST/dirchange/todir/difffile (regular file) + Directory mismatch: $TEST/dirchange/todir/difftype (fifo file) + New link conflict: /second/second/difflinks/link ("new link" vs "test link") + New file mismatch: /second/second/difftype/dir (directory vs fifo file) + Needs update: /etc/mail/aliases.db (requires manual update via newaliases(1)) +EOF + +echo "Differences for -n:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/testn.out \ + || failed=YES + +$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out + +echo "Differences for real:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \ + || failed=YES + +check_trees + +[ "${FAILED}" = no ] diff --git a/usr.sbin/etcupdate/tests/tzsetup_test.sh b/usr.sbin/etcupdate/tests/tzsetup_test.sh new file mode 100644 index 000000000000..155830bddae7 --- /dev/null +++ b/usr.sbin/etcupdate/tests/tzsetup_test.sh @@ -0,0 +1,239 @@ +#!/bin/sh +# +# Copyright (c) 2013 Hudson River Trading LLC +# Written by: John H. Baldwin <jhb@FreeBSD.org> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# + +# Various regression tests for the tzsetup handling in the 'update' command. + +FAILED=no +WORKDIR=work + +usage() +{ + echo "Usage: tzsetup.sh [-s script] [-w workdir]" + exit 1 +} + +# Allow the user to specify an alternate work directory or script. +COMMAND=etcupdate +while getopts "s:w:" option; do + case $option in + s) + COMMAND="sh $OPTARG" + ;; + w) + WORKDIR=$OPTARG + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) +if [ $# -ne 0 ]; then + usage +fi + +CONFLICTS=$WORKDIR/conflicts +OLD=$WORKDIR/old +NEW=$WORKDIR/current +TEST=$WORKDIR/test + +build_trees() +{ + + # Build the base tree, but not /etc/localtime itself + local i j k + + rm -rf $OLD $NEW $TEST $CONFLICTS + mkdir -p $OLD $NEW $TEST + mkdir -p $TEST/etc + mkdir -p $TEST/var/db + mkdir -p $TEST/usr/share/zoneinfo + + # Create a dummy timezone file + echo "foo" > $TEST/usr/share/zoneinfo/foo + +} + +# $1 - relative path to file that should be missing from TEST +missing() +{ + if [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be missing" + FAILED=yes + fi +} + +# $1 - relative path to file that should be a symlink in TEST +# $2 - optional value of the link +link() +{ + local val + + if ! [ -L $TEST/$1 ]; then + echo "File $1 should be a link" + FAILED=yes + elif [ $# -gt 1 ]; then + val=`readlink $TEST/$1` + if [ "$val" != "$2" ]; then + echo "Link $1 should link to \"$2\"" + FAILED=yes + fi + fi +} + +# $1 - relative path to regular file that should be present in TEST +# $2 - optional string that should match file contents +# $3 - optional MD5 of the flie contents, overrides $2 if present +file() +{ + local contents sum + + if ! [ -f $TEST/$1 ]; then + echo "File $1 should be a regular file" + FAILED=yes + elif [ $# -eq 2 ]; then + contents=`cat $TEST/$1` + if [ "$contents" != "$2" ]; then + echo "File $1 has wrong contents" + FAILED=yes + fi + elif [ $# -eq 3 ]; then + sum=`md5 -q $TEST/$1` + if [ "$sum" != "$3" ]; then + echo "File $1 has wrong contents" + FAILED=yes + fi + fi +} + +if [ `id -u` -ne 0 ]; then + echo "must be root" + exit 0 +fi + +if [ -r /etc/etcupdate.conf ]; then + echo "WARNING: /etc/etcupdate.conf settings may break some tests." +fi + +# First, test for /etc/localtime not existing + +build_trees + +$COMMAND -nr -d $WORKDIR -D $TEST > $WORKDIR/testn.out + +cat > $WORKDIR/correct.out <<EOF +EOF + +echo "Differences for no /etc/localtime with -n:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/testn.out \ + || FAILED=yes + +$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out + +echo "Differences for no /etc/localtime:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \ + || FAILED=yes + +missing /etc/localtime +missing /var/db/zoneinfo + +# Second, test for /etc/localtime being a symlink + +build_trees +ln -s /dev/null $TEST/etc/localtime + +$COMMAND -nr -d $WORKDIR -D $TEST > $WORKDIR/testn.out + +cat > $WORKDIR/correct.out <<EOF +EOF + +echo "Differences for symlinked /etc/localtime with -n:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/testn.out \ + || FAILED=yes + +$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out + +echo "Differences for symlinked /etc/localtime:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \ + || FAILED=yes + +link /etc/localtime "/dev/null" +missing /var/db/zoneinfo + +# Third, test for /etc/localtime as a file and a missing /var/db/zoneinfo + +build_trees +echo "bar" > $TEST/etc/localtime + +$COMMAND -nr -d $WORKDIR -D $TEST > $WORKDIR/testn.out + +cat > $WORKDIR/correct.out <<EOF +Warnings: + Needs update: /etc/localtime (required manual update via tzsetup(8)) +EOF + +echo "Differences for missing /var/db/zoneinfo with -n:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/testn.out \ + || FAILED=yes + +$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out + +echo "Differences for missing /var/db/zoneinfo:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \ + || FAILED=yes + +file /etc/localtime "bar" +missing /var/db/zoneinfo + +# Finally, test the case where it should update /etc/localtime + +build_trees +echo "bar" > $TEST/etc/localtime +echo "foo" > $TEST/var/db/zoneinfo + +$COMMAND -nr -d $WORKDIR -D $TEST > $WORKDIR/testn.out + +cat > $WORKDIR/correct.out <<EOF +EOF + +echo "Differences for real update with -n:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/testn.out \ + || FAILED=yes + +$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out + +echo "Differences for real update:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \ + || FAILED=yes + +# XXX tzsetup installs a symlink as of 5e16809c953f +#file /etc/localtime "foo" +#file /var/db/zoneinfo "foo" + +[ "${FAILED}" = no ] |
