summaryrefslogtreecommitdiff
path: root/sysutils
diff options
context:
space:
mode:
authorjmmv <jmmv>2012-08-06 17:06:17 +0000
committerjmmv <jmmv>2012-08-06 17:06:17 +0000
commit9af4961657ad93c44c6af0bd9ca9e711b7ef8f42 (patch)
treed82c8cc6804d32801ad82bd761d743b21f98b881 /sysutils
parent2f42dc2690b9e65df65b086436a4bb26d30a7f58 (diff)
downloadpkgsrc-9af4961657ad93c44c6af0bd9ca9e711b7ef8f42.tar.gz
Initial addition of sysupgrade, version 1.0:
sysupgrade is a script to automate NetBSD system upgrades. sysupgrade works by first fetching distribution sets from a specified site or local directory, then by upgrading the system using such distribution sets and later by ensuring that the system configuration is up to date. All the process is controlled by a configuration file, and the defaults should suit the most common NetBSD upgrades. sysupgrade can be used to perform upgrades across different system major and/or minor versions, and it can also be used to track a stable or development branch from the CVS repository. sysbuild is the perfect companion to sysupgrade in those cases where you want to roll your own binaries: both utilities share a very similar command-line and configuration interface, and the default configuration files provide examples on how to integrate one with the other. A few notes about the import: Right after I submitted sysbuild, I was pointed at etcmanage and its scripts to build and upgrade NetBSD. I am sending this anyway because 1) it matches sysbuild's behavior closely, 2) it has a detailed manual page, 3) it has tests... and, well, 4) I had already written most of it at that time and didn't want to throw it away! The config and utils modules in this import are a duplicate of the code in sysbuild, with a few tweaks. This is really bad and the code should be deduplicated somehow. I'm not sure what the best way of doing so is and can only think about introducing a common base package with the shared code (which brings its own problems). I have tested this to upgrade both -current and 6.0_BETA2 to newer snapshots, both from local and remote release files.
Diffstat (limited to 'sysutils')
-rw-r--r--sysutils/sysupgrade/DESCR15
-rw-r--r--sysutils/sysupgrade/Makefile89
-rw-r--r--sysutils/sysupgrade/PLIST9
-rw-r--r--sysutils/sysupgrade/TODO30
-rw-r--r--sysutils/sysupgrade/files/Kyuafile7
-rw-r--r--sysutils/sysupgrade/files/config.subr299
-rw-r--r--sysutils/sysupgrade/files/config_test.sh422
-rw-r--r--sysutils/sysupgrade/files/default.conf35
-rw-r--r--sysutils/sysupgrade/files/sysupgrade.8387
-rw-r--r--sysutils/sysupgrade/files/sysupgrade.conf.5160
-rw-r--r--sysutils/sysupgrade/files/sysupgrade.sh459
-rw-r--r--sysutils/sysupgrade/files/sysupgrade_test.sh1055
-rw-r--r--sysutils/sysupgrade/files/utils.subr125
-rw-r--r--sysutils/sysupgrade/files/utils_test.sh186
14 files changed, 3278 insertions, 0 deletions
diff --git a/sysutils/sysupgrade/DESCR b/sysutils/sysupgrade/DESCR
new file mode 100644
index 00000000000..74eac4397a9
--- /dev/null
+++ b/sysutils/sysupgrade/DESCR
@@ -0,0 +1,15 @@
+sysupgrade is a script to automate NetBSD system upgrades. sysupgrade
+works by first fetching distribution sets from a specified site or local
+directory, then by upgrading the system using such distribution sets and
+later by ensuring that the system configuration is up to date. All the
+process is controlled by a configuration file, and the defaults should
+suit the most common NetBSD upgrades.
+
+sysupgrade can be used to perform upgrades across different system major
+and/or minor versions, and it can also be used to track a stable or
+development branch from the CVS repository.
+
+sysbuild is the perfect companion to sysupgrade in those cases where you
+want to roll your own binaries: both utilities share a very similar
+command-line and configuration interface, and the default configuration
+files provide examples on how to integrate one with the other.
diff --git a/sysutils/sysupgrade/Makefile b/sysutils/sysupgrade/Makefile
new file mode 100644
index 00000000000..9dc54dde232
--- /dev/null
+++ b/sysutils/sysupgrade/Makefile
@@ -0,0 +1,89 @@
+# $NetBSD: Makefile,v 1.1 2012/08/06 17:06:17 jmmv Exp $
+
+DISTNAME= sysupgrade-1.0
+CATEGORIES= sysutils
+MASTER_SITES= # empty
+DISTFILES= # empty
+
+MAINTAINER= jmmv@NetBSD.org
+COMMENT= Automate upgrades of NetBSD
+LICENSE= modified-bsd
+
+PKG_INSTALLATION_TYPES= overwrite pkgviews
+PKG_DESTDIR_SUPPORT= user-destdir
+
+WRKSRC= ${WRKDIR}
+NO_CONFIGURE= YES
+
+UPGRADE_SUBST+= -e 's,@SYSUPGRADE_CACHEDIR@,${CACHEDIR},g'
+UPGRADE_SUBST+= -e 's,@SYSUPGRADE_ETCDIR@,${PKG_SYSCONFDIR},g'
+
+BUILD_DEFS+= VARBASE
+CACHEDIR= ${VARBASE}/cache/sysupgrade
+OWN_DIRS= ${CACHEDIR} root wheel
+
+EGDIR= ${PREFIX}/share/examples/sysupgrade
+CONF_FILES+= ${EGDIR}/default.conf ${PKG_SYSCONFDIR}/sysupgrade.conf
+
+PKG_OPTIONS_VAR= PKG_OPTIONS.sysupgrade
+PKG_SUPPORTED_OPTIONS= tests
+PKG_SUGGESTED_OPTIONS= tests
+
+.include "../../mk/bsd.options.mk"
+
+.if $(PKG_OPTIONS:Mtests)
+TEST_PROGS= config_test sysupgrade_test utils_test
+
+PLIST_SUBST+= TESTS=
+. include "../../devel/atf-libs/buildlink3.mk"
+
+do-build: build-tests
+build-tests:
+ cp ${FILESDIR}/Kyuafile ${WRKSRC}
+.for file in ${TEST_PROGS}
+ ${ECHO} '#! ${BUILDLINK_PREFIX.atf-libs}/bin/atf-sh' \
+ >${WRKSRC}/${file}
+ ${CAT} ${FILESDIR}/*.subr ${FILESDIR}/${file}.sh \
+ | ${SED} ${UPGRADE_SUBST} >>${WRKSRC}/${file}
+ ${CHMOD} +x ${WRKSRC}/${file}
+.endfor
+
+INSTALLATION_DIRS+= tests/sysupgrade
+
+do-install: install-tests
+install-tests:
+ ${INSTALL_DATA} ${WRKSRC}/Kyuafile ${DESTDIR}${PREFIX}/tests/sysupgrade
+.for file in ${TEST_PROGS}
+ ${INSTALL_SCRIPT} ${WRKSRC}/${file} \
+ ${DESTDIR}${PREFIX}/tests/sysupgrade/
+.endfor
+
+do-test:
+ cd ${WRKSRC} && PATH="${WRKSRC}:${PATH}" kyua test
+.else
+PLIST_SUBST+= TESTS=@comment
+.endif
+
+do-build:
+ ${ECHO} '#! ${SH}' >${WRKSRC}/sysupgrade
+ ${ECHO} 'set -e' >>${WRKSRC}/sysupgrade
+ ${CAT} ${FILESDIR}/*.subr ${FILESDIR}/sysupgrade.sh \
+ | ${SED} ${UPGRADE_SUBST} >>${WRKSRC}/sysupgrade
+ ${ECHO} 'sysupgrade_main "$${@}"' >>${WRKSRC}/sysupgrade
+ ${CHMOD} +x ${WRKSRC}/sysupgrade
+.for file in sysupgrade.8 sysupgrade.conf.5 default.conf
+ sed ${UPGRADE_SUBST} <${FILESDIR}/${file} >${WRKSRC}/${file}
+.endfor
+
+INSTALLATION_DIRS+= bin ${PKGMANDIR}/man5 ${PKGMANDIR}/man8 \
+ share/examples/sysupgrade
+
+do-install:
+ ${INSTALL_SCRIPT} ${WRKSRC}/sysupgrade ${DESTDIR}${PREFIX}/bin/
+ ${INSTALL_MAN} ${WRKSRC}/sysupgrade.8 \
+ ${DESTDIR}${PREFIX}/${PKGMANDIR}/man8/
+ ${INSTALL_MAN} ${WRKSRC}/sysupgrade.conf.5 \
+ ${DESTDIR}${PREFIX}/${PKGMANDIR}/man5/
+ ${INSTALL_DATA} ${WRKSRC}/default.conf ${DESTDIR}${EGDIR}
+
+.include "../../mk/bsd.pkg.mk"
diff --git a/sysutils/sysupgrade/PLIST b/sysutils/sysupgrade/PLIST
new file mode 100644
index 00000000000..2f495d47dff
--- /dev/null
+++ b/sysutils/sysupgrade/PLIST
@@ -0,0 +1,9 @@
+@comment $NetBSD: PLIST,v 1.1 2012/08/06 17:06:17 jmmv Exp $
+bin/sysupgrade
+man/man5/sysupgrade.conf.5
+man/man8/sysupgrade.8
+share/examples/sysupgrade/default.conf
+${TESTS}tests/sysupgrade/Kyuafile
+${TESTS}tests/sysupgrade/config_test
+${TESTS}tests/sysupgrade/sysupgrade_test
+${TESTS}tests/sysupgrade/utils_test
diff --git a/sysutils/sysupgrade/TODO b/sysutils/sysupgrade/TODO
new file mode 100644
index 00000000000..d02130ac626
--- /dev/null
+++ b/sysutils/sysupgrade/TODO
@@ -0,0 +1,30 @@
+- Deduce the current NetBSD release from /etc/release and the target
+ release from etc.tgz and inform the user about the changes. This will be
+ necessary if the upgrade process needs to apply specific tweaks depending
+ on the affected NetBSD releases (which is not the case at the moment).
+
+- Ability to automatically deduce the next upgrade target from a collection
+ of directories (e.g. from FTP). We should be able to tell sysupgrade to
+ follow along 6.0.x, or 6.x, or the daily builds and get it to pick the
+ most recent available build. Having to manually scan FTP directories to
+ select the correct build is... inconvenient.
+
+- Ensure that the fetched sets belong to the current architecture. I have
+ bitten once by mistakenly pointing my custom update scripts to the wrong
+ platform directory, rendering the machine unusable as soon as base.tgz
+ was unpacked.
+
+- Download release checksums and validate files against them. The 'fetch'
+ command should unconditionally download the checksums every time it is
+ run and then deduce whether it needs to redownload (possibly-newer) sets
+ or do nothing.
+
+- Add destdir support to etcupdate(8) and allow the 'etcupdate' command to
+ run when destdir is enabled.
+
+- Maybe sysupgrade should be more interactive by default, letting the user
+ know what exactly is going to happen before doing so (e.g. what will be
+ the new version, where things are being downloaded from, etc.), and
+ providing a "quiet mode" flag instead. etcupdate is interactive anyway,
+ so adding more interactive steps (as long as they can be disabled) does
+ not seem a big deal.
diff --git a/sysutils/sysupgrade/files/Kyuafile b/sysutils/sysupgrade/files/Kyuafile
new file mode 100644
index 00000000000..135b420c442
--- /dev/null
+++ b/sysutils/sysupgrade/files/Kyuafile
@@ -0,0 +1,7 @@
+syntax("kyuafile", 1)
+
+test_suite("sysupgrade")
+
+atf_test_program{name="config_test"}
+atf_test_program{name="sysupgrade_test"}
+atf_test_program{name="utils_test"}
diff --git a/sysutils/sysupgrade/files/config.subr b/sysutils/sysupgrade/files/config.subr
new file mode 100644
index 00000000000..d2f6a90bd35
--- /dev/null
+++ b/sysutils/sysupgrade/files/config.subr
@@ -0,0 +1,299 @@
+# Copyright 2012 Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * 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.
+# * Neither the name of Google Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
+# OWNER 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.
+
+# \file config.subr
+# Configuration file processing and queries.
+
+
+# List of valid configuration variables.
+#
+# This is initialized by config_init and should remain unchanged thorough the
+# execution of the program.
+CONFIG_VARS=
+
+
+# List of overrides to apply by config_apply_overrides.
+#
+# The overrides are recorded and applied separately because they need to happen
+# after a configuration file has been loaded. The contents of this list are
+# words of the form: set:<variable> or unset:<variable>. For those variables
+# listed in set:, there is a corresponding config_override_var_<variable>
+# variable containing the new value.
+CONFIG_OVERRIDES=
+
+
+# Initializes the configuration module.
+#
+# \param ... List of configuration variables to recognize in configuration files
+# and user overrides.
+config_init() {
+ CONFIG_VARS="${@}"
+}
+
+
+# Checks if a configuration variable is known.
+#
+# \param var Name of the variable to check.
+#
+# \return True if the variable was registered by config_init, false otherwise.
+config_is_valid() {
+ local var="${1}"; shift
+
+ local known_var
+ for known_var in ${CONFIG_VARS}; do
+ if [ "${known_var}" = "${var}" ]; then
+ return 0
+ fi
+ done
+ return 1
+}
+
+
+# Checks if a configuration variable is defined.
+#
+# \param var The name of the variable to check.
+#
+# \return True if the variable is defined (even if empty), false otherwise.
+config_has() {
+ local var="${1}"; shift
+
+ local is_unset
+ eval is_unset="\${config_var_${var}-yes_this_is_unset}"
+ if [ "${is_unset}" = yes_this_is_unset ]; then
+ return 1
+ else
+ return 0
+ fi
+}
+
+
+# Gets the value of a defined configuration variable.
+#
+# \post The value of the variable is printed to stdout, if any.
+#
+# \post If the variable is not defined, this terminates execution with an error.
+#
+# \param var Name of the configuration variable to query.
+config_get() {
+ local var="${1}"; shift
+
+ if config_has "${var}"; then
+ eval echo "\${config_var_${var}}"
+ else
+ utils_error "Required configuration variable ${var} not set"
+ fi
+}
+
+
+# Gets the value of configuration variable interpreting it as a boolean.
+#
+# \param var The variable to query.
+#
+# \return True if the variable is set to a truth value, false if its value is
+# false or if it is not defined.
+config_get_bool() {
+ local var="${1}"; shift
+
+ if config_has "${var}"; then
+ case "$(config_get "${var}")" in
+ [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee])
+ return 0
+ ;;
+ [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee])
+ return 1
+ ;;
+ *)
+ utils_error "Invalid boolean value in variable ${var}"
+ ;;
+ esac
+ else
+ return 1
+ fi
+}
+
+
+# Gets the value of a configuration variable with a default fallback.
+#
+# \post The value of the variable is printed to stdout, if any.
+#
+# \param var Name of the configuration variable to query.
+# \param default Default value to return if the variable is not defined.
+config_get_default() {
+ local var="${1}"; shift
+ local default="${1}"; shift
+
+ if config_has "${var}"; then
+ config_get "${var}"
+ else
+ echo "${default}"
+ fi
+}
+
+
+# Sets a configuration variable.
+#
+# \post Execution terminates if the variable is not valid.
+#
+# \param var The name of the configuration variable to set.
+# \param value The value to set.
+config_set() {
+ local var="${1}"; shift
+ local value="${1}"; shift
+
+ config_is_valid "${var}" \
+ || utils_usage_error "Unknown configuration variable ${var}"
+ eval "config_var_${var}=\"\${value}\""
+}
+
+
+# Unsets a configuration variable.
+#
+# \param var The name of the configuration variable to unset.
+config_unset() {
+ local var="${1}"; shift
+
+ config_is_valid "${var}" \
+ || utils_usage_error "Unknown configuration variable ${var}"
+ eval unset "config_var_${var}"
+}
+
+
+# Loads a configuration file.
+#
+# \pre config_init should have been called to register the valid configuration
+# variables. Any non-registered configuration variable found in the file will
+# not be available through any of the functions in this module.
+#
+# \post The configuration module is updated with the values defined in the
+# configuration file.
+#
+# \post Any errors in the processing of the configuration file terminate the
+# execution of the script.
+#
+# \param config_file Path to the file to load.
+config_load() {
+ local config_file="${1}"; shift
+
+ [ -e "${config_file}" ] || utils_error "Configuration file ${config_file}" \
+ "does not exist"
+
+ # User-facing variables.
+ local var
+ for var in ${CONFIG_VARS}; do
+ unset "${var}" || true
+ eval local "${var}"
+ done
+
+ local real_config_file
+ case "${config_file}" in
+ */*) real_config_file="${config_file}" ;;
+ *) real_config_file="./${config_file}" ;;
+ esac
+ ( . "${real_config_file}" ) || utils_error "Failed to load configuration" \
+ "file ${config_file}"
+ . "${real_config_file}"
+
+ for var in ${CONFIG_VARS}; do
+ local value
+ eval value="\${${var}-unset}"
+ [ "${value-unset}" != unset ] || continue
+
+ if [ -n "${value}" ]; then
+ config_set "${var}" "${value}"
+ else
+ unset "config_var_${var}" || true
+ fi
+ done
+
+ config_apply_overrides
+}
+
+
+# Applies recorded overrides to the current configuration.
+#
+# This is just a helper function for config_load and should not be used
+# directly.
+#
+# \post The configuration data in memory is modified according to the data
+# recorded in the overrides.
+#
+# \post The contents of CONFIG_OVERRIDES is cleared.
+config_apply_overrides() {
+ for override in ${CONFIG_OVERRIDES}; do
+ case "${override}" in
+ set:*)
+ local var="$(echo ${override} | cut -d : -f 2)"
+ local value
+ eval value="\"\${config_override_var_${var}}\""
+ config_set "${var}" "${value}"
+ ;;
+ unset:*)
+ local var="$(echo ${override} | cut -d : -f 2)"
+ unset "config_var_${var}" || true
+ ;;
+ esac
+ done
+ CONFIG_OVERRIDES=
+}
+
+
+# Records an override to be applied to the configuration.
+#
+# Overrides are configuration variables set through the command line. These can
+# be set before loading the configuration file with config_load.
+#
+# \post Any errors in the processing of the configuration override terminate the
+# execution of the script.
+#
+# \param arg An override of the form variable=value.
+config_override() {
+ local arg="${1}"; shift
+
+ case "${arg}" in
+ *=*)
+ ;;
+ *)
+ utils_usage_error "Invalid configuration override" \
+ "${arg}; must be of the form variable=value"
+ ;;
+ esac
+ local var="$(echo "${arg}" | cut -d = -f 1)"
+ local value="$(echo "${arg}" | cut -d = -f 2-)"
+
+ [ -n "${var}" ] || utils_usage_error "Invalid configuration override" \
+ "${arg}; must be of the form variable=value"
+ config_is_valid "${var}" \
+ || utils_usage_error "Unknown configuration variable ${var}"
+
+ if [ -n "${value}" ]; then
+ eval "config_override_var_${var}=\"${value}\""
+ CONFIG_OVERRIDES="${CONFIG_OVERRIDES} set:${var}"
+ else
+ CONFIG_OVERRIDES="${CONFIG_OVERRIDES} unset:${var}"
+ fi
+}
diff --git a/sysutils/sysupgrade/files/config_test.sh b/sysutils/sysupgrade/files/config_test.sh
new file mode 100644
index 00000000000..0de28814523
--- /dev/null
+++ b/sysutils/sysupgrade/files/config_test.sh
@@ -0,0 +1,422 @@
+# Copyright 2012 Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * 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.
+# * Neither the name of Google Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
+# OWNER 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.
+
+
+atf_test_case is_valid__true
+is_valid__true_body() {
+ config_init VAR1 VAR3
+ for var in VAR1 VAR3; do
+ config_is_valid "${var}" || atf_fail "${var} not found"
+ done
+}
+
+
+atf_test_case is_valid__false
+is_valid__false_body() {
+ config_init VAR1 VAR3
+ for var in VAR11 VAR2 VAR; do
+ if config_is_valid "${var}"; then
+ atf_fail "${var} found but was not registered"
+ fi
+ done
+}
+
+
+atf_test_case has__true__empty
+has__true__empty_body() {
+ config_init TESTVAR
+ config_set TESTVAR ""
+ config_has TESTVAR || atf_fail "Expected variable not found"
+}
+
+
+atf_test_case has__true__not_empty
+has__true__not_empty_body() {
+ config_init TESTVAR
+ config_set TESTVAR "foo"
+ config_has TESTVAR || atf_fail "Expected variable not found"
+}
+
+
+atf_test_case has__false
+has__false_body() {
+ config_init TESTVAR
+ if config_has TESTVAR; then
+ atf_fail "Unexpected variable found"
+ fi
+}
+
+
+atf_test_case get__ok__empty
+get__ok__empty_body() {
+ config_init TESTVAR
+
+ config_set TESTVAR ""
+ [ -z "$(config_get TESTVAR)" ] || atf_fail "Failed to query value"
+}
+
+
+atf_test_case get__ok__not_empty
+get__ok__not_empty_body() {
+ config_init TESTVAR
+
+ config_set TESTVAR some-value
+ [ "$(config_get TESTVAR)" = some-value ] || atf_fail "Failed to query value"
+}
+
+
+atf_test_case get__undefined_variable
+get__undefined_variable_body() {
+ config_init TESTVAR
+
+ if ( config_get TESTVAR ) >out 2>err; then
+ atf_fail "Got unset variable successfully"
+ else
+ grep "Required configuration variable TESTVAR not set" err >/dev/null \
+ || atf_fail "Expected error message not found"
+ fi
+}
+
+
+atf_test_case get_bool__true
+get_bool__true_body() {
+ config_init TESTVAR
+
+ for value in yes Yes true True; do
+ config_set TESTVAR "${value}"
+ config_get_bool TESTVAR || atf_fail "Expected true, but got false"
+ done
+}
+
+
+atf_test_case get_bool__false
+get_bool__false_body() {
+ config_init TESTVAR
+
+ for value in no No false False; do
+ config_set TESTVAR "${value}"
+ if config_get_bool TESTVAR; then
+ atf_fail "Expected false, but got true"
+ fi
+ done
+}
+
+
+atf_test_case get_bool__undefined_variable
+get_bool__undefined_variable_body() {
+ config_init TESTVAR
+
+ if config_get_bool TESTVAR; then
+ atf_fail "Expected false, but got true"
+ fi
+}
+
+
+atf_test_case get_bool__invalid_value
+get_bool__invalid_value_body() {
+ config_init TESTVAR
+
+ config_set TESTVAR not-a-boolean
+ if ( config_get_bool TESTVAR ) >out 2>err; then
+ atf_fail "Got invalid boolean value successfully"
+ else
+ grep "Invalid boolean value in variable TESTVAR" err >/dev/null \
+ || atf_fail "Expected error message not found"
+ fi
+}
+
+
+atf_test_case get_default__defined__empty
+get_default__defined__empty_body() {
+ config_init TESTVAR
+ config_set TESTVAR ""
+ [ "$(config_get_default TESTVAR 'foo')" = "" ] \
+ || atf_fail "Did not fetch defined value"
+}
+
+
+atf_test_case get_default__defined__not_empty
+get_default__defined__not_empty_body() {
+ config_init TESTVAR
+ config_set TESTVAR "bar"
+ [ "$(config_get_default TESTVAR 'foo')" = "bar" ] \
+ || atf_fail "Did not fetch defined value"
+}
+
+
+atf_test_case get_default__default__empty
+get_default__default__empty_body() {
+ config_init TESTVAR
+ [ "$(config_get_default TESTVAR '')" = "" ] \
+ || atf_fail "Did not fetch default value"
+}
+
+
+atf_test_case get_default__default__not_empty
+get_default__default__not_empty_body() {
+ config_init TESTVAR
+ [ "$(config_get_default TESTVAR 'foo')" = "foo" ] \
+ || atf_fail "Did not fetch default value"
+}
+
+
+atf_test_case set__ok
+set__ok_body() {
+ config_init TESTVAR
+
+ config_set TESTVAR some-value
+ [ "${config_var_TESTVAR}" = some-value ] || atf_fail "Failed to set value"
+}
+
+
+atf_test_case set__unknown_variable
+set__unknown_variable_body() {
+ config_init TESTVAR
+
+ if ( config_set TESTVAR2 some-value ) >out 2>err; then
+ atf_fail "Set unknown variable successfully"
+ else
+ grep "Unknown configuration variable TESTVAR2" err >/dev/null \
+ || atf_fail "Expected error message not found"
+ fi
+}
+
+
+atf_test_case unset__ok
+unset__ok_body() {
+ config_init TESTVAR
+
+ config_var_TESTVAR=some-value
+ config_unset TESTVAR
+ [ "${config_var_TESTVAR-unset}" = unset ] \
+ || atf_fail "Failed to unset variable"
+}
+
+
+atf_test_case unset__unknown_variable
+unset__unknown_variable_body() {
+ config_init TESTVAR
+
+ if ( config_unset TESTVAR2 ) >out 2>err; then
+ atf_fail "Unset unknown variable successfully"
+ else
+ grep "Unknown configuration variable TESTVAR2" err >/dev/null \
+ || atf_fail "Expected error message not found"
+ fi
+}
+
+
+atf_test_case load__filter_variables
+load__filter_variables_body() {
+ config_init Z VAR1 EMPTY
+
+ cat >test.conf <<EOF
+A=foo
+Z=bar
+VAR1="some text"
+VAR2="some other text"
+EOF
+
+ config_load $(pwd)/test.conf || atf_fail "Failed to load test configuration"
+
+ [ "${config_var_Z}" = bar ] || \
+ atf_fail "Z not found in configuration"
+ [ "${config_var_VAR1}" = "some text" ] || \
+ atf_fail "VAR1 not found in configuration"
+
+ [ "${config_var_EMPTY-has_not_been_set}" = has_not_been_set ] || \
+ atf_fail "Undefined variable set, but should not have been"
+
+ [ "${config_var_A-unset}" = unset ] || \
+ atf_fail "A set in configuration, but not expected"
+ [ "${config_var_VAR2-unset}" = unset ] || \
+ atf_fail "VAR2 set in configuration, but not expected"
+}
+
+
+atf_test_case load__allow_undefine
+load__allow_undefine_body() {
+ config_init UNDEFINE
+
+ cat >test.conf <<EOF
+UNDEFINE=
+EOF
+
+ config_set UNDEFINE "remove me"
+ config_load $(pwd)/test.conf || atf_fail "Failed to load test configuration"
+ if config_has UNDEFINE; then
+ atf_fail "Undefine attempt from configuration did not work"
+ fi
+}
+
+
+atf_test_case load__current_directory
+load__current_directory_body() {
+ config_init A
+
+ cat >test.conf <<EOF
+A=foo
+EOF
+
+ config_load test.conf || atf_fail "Failed to load test configuration"
+
+ [ "${config_var_A}" = foo ] || \
+ atf_fail "A not found in configuration"
+}
+
+
+atf_test_case load__missing_file
+load__missing_file_body() {
+ if ( config_load missing.conf ) >out 2>err; then
+ atf_fail "Missing configuration file load succeeded"
+ else
+ grep "Configuration file missing.conf does not exist" err >/dev/null \
+ || atf_fail "Expected error message not found"
+ fi
+}
+
+
+atf_test_case load__invalid_file
+load__invalid_file_body() {
+ echo "this file is invalid" >invalid.conf
+
+ if ( config_load invalid.conf ) >out 2>err; then
+ atf_fail "Invalid configuration file load succeeded"
+ else
+ cat err
+ grep "Failed to load configuration file invalid.conf" err >/dev/null \
+ || atf_fail "Expected error message not found"
+ fi
+}
+
+
+atf_test_case override__ok_before_load
+override__ok_before_load_body() {
+ config_init VAR1 VAR2
+
+ cat >test.conf <<EOF
+VAR1="override me"
+VAR2="do not override me"
+EOF
+
+ config_override "VAR1=new value"
+ config_load test.conf || atf_fail "Failed to load test configuration"
+
+ [ "${config_var_VAR1}" = "new value" ] || atf_fail "Override failed"
+ [ "${config_var_VAR2}" = "do not override me" ] \
+ || atf_fail "Overrode more than one variable"
+}
+
+
+atf_test_case override__not_ok_after_load
+override__not_ok_after_load_body() {
+ config_init VAR1 VAR2
+
+ cat >test.conf <<EOF
+VAR1="override me"
+VAR2="do not override me"
+EOF
+
+ config_load test.conf || atf_fail "Failed to load test configuration"
+ config_override "VAR1=new value"
+
+ [ "${config_var_VAR1}" = "override me" ] \
+ || atf_fail "Override succeeded, but it should not have"
+ [ "${config_var_VAR2}" = "do not override me" ] \
+ || atf_fail "Overrode more than one variable"
+}
+
+
+atf_test_case override__invalid_format
+override__invalid_format_body() {
+ for arg in foo =bar ''; do
+ if ( config_override "${arg}" ) >out 2>err; then
+ atf_fail "Invalid configuration override ${arg} succeeded"
+ else
+ cat err
+ grep "Invalid configuration override ${arg}" err >/dev/null \
+ || atf_fail "Expected error message not found"
+ fi
+ done
+}
+
+
+atf_test_case override__unknown_variable
+override__unknown_variable_body() {
+ config_init Z VAR1
+ for arg in A=b VAR2=d; do
+ if ( config_override "${arg}" ) >out 2>err; then
+ atf_fail "Invalid configuration override ${arg} succeeded"
+ else
+ cat err
+ grep "Unknown configuration variable ${var}" err >/dev/null \
+ || atf_fail "Expected error message not found"
+ fi
+ done
+}
+
+
+atf_init_test_cases() {
+ atf_add_test_case is_valid__true
+ atf_add_test_case is_valid__false
+
+ atf_add_test_case has__true__empty
+ atf_add_test_case has__true__not_empty
+ atf_add_test_case has__false
+
+ atf_add_test_case get__ok__empty
+ atf_add_test_case get__ok__not_empty
+ atf_add_test_case get__undefined_variable
+
+ atf_add_test_case get_bool__true
+ atf_add_test_case get_bool__false
+ atf_add_test_case get_bool__undefined_variable
+ atf_add_test_case get_bool__invalid_value
+
+ atf_add_test_case get_default__defined__empty
+ atf_add_test_case get_default__defined__not_empty
+ atf_add_test_case get_default__default__empty
+ atf_add_test_case get_default__default__not_empty
+
+ atf_add_test_case set__ok
+ atf_add_test_case set__unknown_variable
+
+ atf_add_test_case unset__ok
+ atf_add_test_case unset__unknown_variable
+
+ atf_add_test_case load__filter_variables
+ atf_add_test_case load__allow_undefine
+ atf_add_test_case load__current_directory
+ atf_add_test_case load__missing_file
+ atf_add_test_case load__invalid_file
+
+ atf_add_test_case override__ok_before_load
+ atf_add_test_case override__not_ok_after_load
+ atf_add_test_case override__invalid_format
+ atf_add_test_case override__unknown_variable
+}
diff --git a/sysutils/sysupgrade/files/default.conf b/sysutils/sysupgrade/files/default.conf
new file mode 100644
index 00000000000..f1c665416a2
--- /dev/null
+++ b/sysutils/sysupgrade/files/default.conf
@@ -0,0 +1,35 @@
+# $NetBSD: default.conf,v 1.1 2012/08/06 17:06:17 jmmv Exp $
+
+# Configuration of automatic system upgrades by sysupgrade(8).
+#
+# See sysupgrade.conf(5) for details on the syntax of this file and the
+# meaning of the configuration variables. Note that not all the supported
+# configuration variables are listed in this sample configuration file.
+
+# Path to the release files (local path or remote URL).
+#RELEASEDIR="ftp://ftp.NetBSD.org/pub/NetBSD/NetBSD-6.0/$(uname -m)"
+RELEASEDIR="/home/sysbuild/release/$(uname -m)"
+
+# Name of the kernel to be installed.
+KERNEL=AUTO # Guess from /netbsd (requires config(1)).
+
+# Whitespace-separated list of sets to install.
+SETS=AUTO # Guess from /etc/mtree/set.* files.
+
+# Whitespace-separated list of postinstall(8) checks to automatically fix.
+POSTINSTALL_AUTOFIX="obsolete"
+
+# Whether to run etcupdate or not as part of an upgrade.
+#
+# Running etcupdate is the only interactive step in the upgrade process, so
+# setting this variable to 'no' effectively makes upgrades unattended. You
+# can later run etcupdate at a later step by hand.
+#ETCUPDATE=no
+
+# Whether to delete the downloaded files after an upgrade or not.
+#
+# If you set ETCUPDATE=no, you will most likely want to disable
+# auto-cleaning as well. The reason for this is that, if you want to later
+# do "sysupgrade etcupdate" using the same etc.tgz file downloaded during
+# the upgrade, you will need the same etc.tgz file to be present.
+#AUTOCLEAN=no
diff --git a/sysutils/sysupgrade/files/sysupgrade.8 b/sysutils/sysupgrade/files/sysupgrade.8
new file mode 100644
index 00000000000..e90ea72ff4b
--- /dev/null
+++ b/sysutils/sysupgrade/files/sysupgrade.8
@@ -0,0 +1,387 @@
+.\" Copyright 2012 Google Inc.
+.\" All rights reserved.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions are
+.\" met:
+.\"
+.\" * Redistributions of source code must retain the above copyright
+.\" notice, this list of conditions and the following disclaimer.
+.\" * 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.
+.\" * Neither the name of Google Inc. nor the names of its contributors
+.\" may be used to endorse or promote products derived from this software
+.\" without specific prior written permission.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
+.\" OWNER 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 August 6, 2012
+.Dt SYSUPGRADE 8
+.Os
+.Sh NAME
+.Nm sysupgrade
+.Nd upgrades a NetBSD system to a newer version
+.Sh SYNOPSIS
+common_flags ::=
+.Op Fl c Ar config_name
+.Op Fl d Ar destdir
+.Op Fl o Ar variable=value
+.Pp
+.Em Major commands :
+.Pp
+.Nm
+.Op common_flags
+.Ar auto
+.Op Ar releasedir
+.Nm
+.Op common_flags
+.Ar config
+.Op Fl a
+.Pp
+.Em Standalone upgrade steps :
+.Pp
+.Nm
+.Op common_flags
+.Ar clean
+.Nm
+.Op common_flags
+.Ar etcupdate
+.Nm
+.Op common_flags
+.Ar fetch
+.Op Ar releasedir
+.Nm
+.Op common_flags
+.Ar kernel
+.Op Ar kernel_name
+.Nm
+.Op common_flags
+.Ar modules
+.Nm
+.Op common_flags
+.Ar sets
+.Op Ar set1 .. setN
+.Nm
+.Op common_flags
+.Ar postinstall
+.Op Ar arg1 .. argN
+.Sh DESCRIPTION
+.Nm
+is a utility that automates the process of upgrading a possibly-running
+.Nx
+system to a newer release.
+.Pp
+.Nm
+works by first fetching the release sets from a remote site or from a local
+directory, then by upgrading the system using such release sets and finally by
+taking care of bringing the system configuration up to date.
+In other words,
+.Nm
+does nothing special on its own: it is just a utility that automates a highly
+manual process and relies on other tools within the system to perform its job.
+.Pp
+.Nm
+has a subcommand-based interface: every command performs a single step of the
+upgrade procedure, and the
+.Sq auto
+command orchestrates a complete upgrade by invoking the rest of the commands in
+a specific order.
+There are a set of options that apply to all commands (those stated before the
+command name), and every particular command may accept its own options and
+arguments as shown in the synopsis.
+.Pp
+The behavior of
+.Nm
+is defined by a configuration file that specifies how to apply an update to the
+system (see
+.Xr sysupgrade.conf 5 ) .
+For example, the configuration states which distribution sets ought to be
+installed, where they need to be downloaded from, and whether the system
+configuration files should be upgraded.
+.Pp
+The following options apply to all commands:
+.Bl -tag -width XoXvariableXvalueXX
+.It Fl c Ar config_file
+Specifies the configuration file to use.
+.Pp
+Default:
+.Pa @SYSUPGRADE_ETCDIR@/sysupgrade.conf
+.It Fl d Ar destdir
+Pato the the
+.Nx
+system to upgrade.
+.Pp
+This optional flag can be used to upgrade a non-live system or for
+testing/development purposes.
+Note that some steps (particularly
+.Sq etcupdate )
+do not support this feature and thus will never be run if set.
+.Pp
+Default: not set (which means
+.Pa /
+is affected).
+.It Fl o Ar variable=value
+Applies an override to the loaded configuration.
+.Pp
+The
+.Ar variable
+part of the argument must be any of the recognized configuration variables
+described in
+.Xr sysupgrade.conf 5 .
+The
+.Ar value ,
+if not empty, specifies the value to set the configuration variable to.
+If
+.Ar value
+is empty, then the configuration variable is unset.
+.El
+.Ss The auto command
+The auto command is the most important command in
+.Nm ,
+and is probably the one you will find yourself using most frequently.
+This is the command that takes care of upgrading a full
+.Nx
+installation, and it does so by invoking the other commands in the tool in the
+specific order in which they are needed.
+.Pp
+The optional argument
+.Ar releasedir
+points to the release directory or URL to use, overriding the value of
+.Va RELEASEDIR
+in the configuration file (if any).
+If you are tracking daily builds from an FTP site, for example, you will
+probably want to avoid setting
+.Va RELEASEDIR
+in the configuration file and instead pass an URL each time you run this
+command.
+.Pp
+The standard upgrade procedure performed by this command is as follows:
+.Bl -enum
+.It
+.Sq fetch :
+Retrieve distribution sets into the local cache directory.
+.It
+.Sq modules :
+Unpack new kernel modules.
+.It
+.Sq kernel :
+Upgrade kernel.
+.It
+.Sq sets :
+Upgrade system sets, except configuration files.
+.It
+.Sq etcupdate :
+Merge new changes to configuration files.
+This is the only interactive step in the process and can be disabled for this
+reason by setting
+.Va ETCUPDATE
+to false.
+.It
+.Sq postinstall :
+Perform sanity checks and optionally apply unconditional fixes to the upgraded
+system.
+.It
+.Sq clean :
+Remove contents of the cache directory.
+This can be disabled by setting
+.Va AUTOCLEAN
+to false in case you want to keep the downloaded distribution files around.
+.El
+.Ss The clean command
+The clean command removes any distribution sets from the local cache directory.
+.Ss The config command
+The config command dumps the loaded configuration to the standard output.
+The format of the output is not a script, so it cannot be fed back into
+.Nm .
+The purpose of this command is to aid in debugging the configuration of the
+tool before performing any builds, particularly when the configuration
+files use shell logic to determine the value of any variables.
+.Pp
+The following options are accepted:
+.Bl -tag -width XaXX
+.It Fl a
+Process automatic variables such as
+.Va KERNEL
+and
+.Va SETS
+and display their deduced values instead of only showing the literal
+.Sq AUTO .
+.El
+.Ss The etcupdate command
+The etcupdate command invokes
+.Xr etcupdate 8
+to perform an interactive upgrade of the system configuration files in
+.Pa /etc .
+This is the only interactive process in a full system upgrade.
+.Pp
+In order for this command to do anything useful, the list of sets to install as
+specified by the
+.Va SETS
+configuration variable
+.Em must include
+.Sq etc ,
+and may optionally include
+.Sq xetc .
+.Pp
+Note that, due to defficiencies in the
+.Xr etcupdate 8
+utility, this command does not work when
+.Va DESTDIR
+is set.
+.Ss The fetch command
+The fetch command gets a copy of the release sets as pointed to by the
+.Va RELEASEDIR
+variable into a local cache directory, or from the directory indicated by the
+optional argument
+.Ar releasedir .
+.Pp
+If
+.Va RELEASEDIR
+points to a local directory, the utility just creates symlinks within the cache
+directory pointing to the original files.
+Otherwise, if
+.Va RELEASEDIR
+points to a remote FTP or HTTP site, the fetch command will retrieve the
+contents of the release directory into the local cache directory.
+.Pp
+Please note that all the commands that access distribution sets do so by looking
+for such files in the cache directory
+.Em even when the release directory is in a local path .
+This means that, for such commands to work, you must run fetch beforehand.
+.Ss The kernel command
+The kernel command upgrades the kernel to a newer version.
+The kernel to install is determined by the optional argument
+.Ar kernel_name
+if present, or otherwise from the
+.Va KERNEL
+variable.
+The kernel is expected to be found in a
+.Sq kern-<NAME>
+distribution set in the release directory.
+.Pp
+The previous kernel is backed up as
+.Pa <destdir>/onetbsd.
+.Ss The modules command
+The modules command upgrades the kernel modules to a newer version.
+This operation only takes place if the sets to be installed, as specified by the
+.Va SETS
+variable, contains the
+.Sq modules
+set.
+.Ss The sets command
+The sets command upgrades all non-kernel, non-modules and non-configuration sets
+of the system to a newer version.
+In other words, this command installs sets like
+.Sq base
+or
+.Sq tests
+but explicitly skips sets of the forms
+.Sq *etc ,
+.Sq kern-*
+and
+.Sq modules .
+.Pp
+The list of sets to be installed is determined by the optional arguments passed
+to the command or, if none, from the value of the
+.Va SETS
+configuration variable.
+.Ss The postinstall command
+The postinstall command invokes the
+.Xr postinstall 8
+utility to perform checks and fixes on the system after all new files have been
+put in place.
+.Pp
+The
+.Va POSTINSTALL_AUTOFIX
+variable can optionally include a list of
+.Xr postinstall 8
+fixes to be applied to a system unconditionally.
+For example, listing
+.Sq obsolete
+in this variable is usually useful as this check fails often during upgrades and
+is safe to auto-fix.
+.Pp
+Any arguments supplied to the command are passed directly to
+.Xr postinstall 8 ,
+which comes handy in those cases where you have to manually fix a broken test.
+.Sh FILES
+.Bl -tag -width XXXX
+.It Pa @SYSUPGRADE_ETCDIR@/sysupgrade.conf
+Default configuration file.
+.It Pa @SYSUPGRADE_CACHEDIR@
+Location where distribution sets are temporarily stored.
+The
+.Sq fetch
+command writes files into this directory and the
+.Sq clean
+command clears its contents.
+.It Pa /home/sysbuild/release/<machine>
+Standard location of the releases built by the
+.Xr sysbuild 1
+utility.
+.El
+.Sh EXAMPLES AND TROUBLESHOOTING
+The most common way of executing
+.Nm
+is by using the
+.Sq auto
+command and relying in the default configuration file:
+.Bd -literal -offset indent
+$ sysupgrade auto
+.Ed
+.Pp
+If you wish to track minor stable releases, you may want to do something like
+this every time a new release is published:
+.Bd -literal -offset indent
+$ sysupgrade auto \\
+ ftp://ftp.NetBSD.org/pub/NetBSD/NetBSD-6.<minor>/$(uname -m)
+.Ed
+.Pp
+If the upgrade process fails due to an aborted FTP or HTTP connection, simply
+rerun
+.Nm
+in
+.Sq auto
+mode and it will resume the download where it left off.
+.Pp
+If the
+.Sq postinstall
+step fails due to broken checks, you can manually resume that stage and complete
+the upgrade by doing:
+.Bd -literal -offset indent
+$ sysupgrade postinstall fix <names of the failed checks>
+$ sysupgrade clean
+.Ed
+.Pp
+If you have decided to run
+.Xr etcupdate 8
+by hand separately from
+.Nm ,
+you could do:
+.Bd -literal -offset indent
+$ sysupgrade -o AUTOCLEAN=no -o ETCUPDATE=no auto
+\&... and later, at your earliest convenience ...
+$ sysupgrade etcupdate
+$ sysupgrade clean
+.Ed
+.Sh SEE ALSO
+.Xr sysbuild 1 ,
+.Xr sysupgrade.conf 5 ,
+.Xr etcupdate 8 ,
+.Xr postinstall 8 .
+.Sh AUTHORS
+The
+.Nm
+utility was developed by
+.An Julio Merino
+.Aq jmmv@NetBSD.org .
diff --git a/sysutils/sysupgrade/files/sysupgrade.conf.5 b/sysutils/sysupgrade/files/sysupgrade.conf.5
new file mode 100644
index 00000000000..1df328fc95d
--- /dev/null
+++ b/sysutils/sysupgrade/files/sysupgrade.conf.5
@@ -0,0 +1,160 @@
+.\" Copyright 2012 Google Inc.
+.\" All rights reserved.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions are
+.\" met:
+.\"
+.\" * Redistributions of source code must retain the above copyright
+.\" notice, this list of conditions and the following disclaimer.
+.\" * 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.
+.\" * Neither the name of Google Inc. nor the names of its contributors
+.\" may be used to endorse or promote products derived from this software
+.\" without specific prior written permission.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
+.\" OWNER 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 August 6, 2012
+.Dt SYSUPGRADE.CONF 5
+.Os
+.Sh NAME
+.Nm sysupgrade.conf
+.Nd configuration file to control NetBSD system upgrades
+.Sh DESCRIPTION
+Configuration files for
+.Xr sysupgrade 8
+are simple shell scripts that set, or explicitly clear, a collection of known
+configuration variables.
+.Pp
+The following configuration variables are recognized:
+.Bl -tag
+.It Va AUTOCLEAN
+Whether to automatically delete the downloaded files or not as part of the
+.Sq auto
+command.
+.Pp
+If you set
+.Va ETCUPDATE
+to false, you will probably want to disable autocleaning.
+This will let you run the
+.Sq etcupdate
+command on your own at a later stage reusing the same distribution sets used to
+upgrade the system.
+.Pp
+Default: yes.
+.It Va CACHEDIR
+Path to the local directory into which distribution sets are temporarily fetched
+while applying a system upgrade.
+Note that all
+.Nm
+commands that need access to the distribution sets (pretty much all of them)
+will read from this directory.
+If you plan on issuing different upgrade steps at different times, this
+directory should persist across the different executions.
+.Pp
+Default:
+.Pa @SYSUPGRADE_CACHEDIR@
+.It Va DESTDIR
+Path to the root of the system to be upgraded.
+This is prepended to all other paths affected by
+.Xr sysupgrade 8
+and can be used to perform upgrades of non-live systems or for testing
+purposes.
+.Pp
+Default: not set (which means
+.Pa /
+is affected).
+.It Va ETCUPDATE
+Whether to run
+.Xr etcupdate 8
+as part of an automated upgrade.
+You may want to disable this because this is the only interactive step in the
+upgrade process.
+.Pp
+If set to false, the
+.Sq auto
+subcommand will skip this step.
+In this case, you are also recommended to set
+.Va AUTOCLEAN
+to
+.Sq no
+so that you are not required to redownload the distribution files just to run
+.Xr etcupdate 8
+later.
+.Pp
+Default: yes.
+.It Va KERNEL
+Name of the kernel to install.
+There must be a
+.Sq kern-<KERNEL>
+distribution set in the release directory matching this name.
+.Pp
+If this is set to the magic value
+.Sq AUTO ,
+.Xr sysupgrade 8
+will attempt to automatically determine the name of the current kernel by using
+.Xr config 1
+on
+.Pa <DESTDIR>/netbsd .
+If the guessing fails, an error is raised to prevent installing a mismatching
+kernel.
+.Pp
+Default: AUTO.
+.It Va POSTINSTALL_AUTOFIX
+Whitespace-separated list of
+.Xr postinstall 8
+checks that will be unconditionally fixed as part of an upgrade.
+.Pp
+It is recommended that you include
+.Sq obsolete
+in this list at the very least: this check often fails because it is common for
+newer releases to drop existing files, and fixing it should be harmless.
+Note that old shared libraries are never cleaned by this check, so existing
+third-party binaries should not stop working even after auto-fixing this check.
+.Pp
+Default: empty.
+.It Va RELEASEDIR
+Location of the release directory (which must hold a
+.Pa binary/sets/
+subdirectory).
+This can be either a local path or a URL.
+.Pp
+Default: not set.
+.It Va SETS
+Whitespace-separated list of distribution sets to install.
+If this is set to the magic value
+.Sq AUTO ,
+the list of sets is determined from the files in
+.Pa <DESTDIR>/etc/mtree .
+.Pp
+This list should not include any
+.Sq kern-*
+sets as this is automatically handled by the
+.Va KERNEL
+variable.
+.Pp
+The presence of any configuration sets (such as
+.Sq etc
+or
+.Sq xetc )
+in this list enables the etcupdate and postinstall steps in the automated
+upgrade procedure.
+.Pp
+The presence of a modules set enables the modules step in the automated upgrade
+procedure.
+.Pp
+Default: AUTO.
+.El
+.Sh SEE ALSO
+.Xr sysupgrade 8 .
diff --git a/sysutils/sysupgrade/files/sysupgrade.sh b/sysutils/sysupgrade/files/sysupgrade.sh
new file mode 100644
index 00000000000..78b97605521
--- /dev/null
+++ b/sysutils/sysupgrade/files/sysupgrade.sh
@@ -0,0 +1,459 @@
+# Copyright 2012 Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * 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.
+# * Neither the name of Google Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
+# OWNER 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.
+
+# \file sysupgrade.sh
+# Entry point and main program logic.
+
+
+# List of valid configuration variables.
+#
+# Please remember to update sysbuild(1) if you change this list.
+SYSUPGRADE_CONFIG_VARS="AUTOCLEAN CACHEDIR DESTDIR ETCUPDATE KERNEL
+ POSTINSTALL_AUTOFIX RELEASEDIR SETS"
+
+
+# Directory in which to keep downloaded release files.
+#
+# Can be overriden for test purposes only.
+: ${SYSUPGRADE_CACHEDIR:="@SYSUPGRADE_CACHEDIR@"}
+
+
+# Paths to installed files.
+#
+# Can be overriden for test purposes only.
+: ${SYSUPGRADE_ETCDIR:="@SYSUPGRADE_ETCDIR@"}
+
+
+# Sets defaults for configuration variables that need a value.
+#
+# This function should be called before the configuration file has been loaded.
+# This means that the user can undefine a required configuration variable, but
+# we let him shoot himself in the foot if he so desires.
+sysupgrade_set_defaults() {
+ # Please remember to update sysupgrade(8) if you change any default values.
+ config_set AUTOCLEAN "yes"
+ config_set CACHEDIR "${SYSUPGRADE_CACHEDIR}"
+ config_set ETCUPDATE "yes"
+ config_set KERNEL "AUTO"
+ config_set SETS "AUTO"
+}
+
+
+# Interprets magic configuration values.
+#
+# This function should be called after the configuration file has been loaded.
+# It takes care of performing any required post-processing on the configuration
+# variables, such as expanding the magic AUTO keyword to the actual value.
+sysupgrade_auto_config() {
+ if [ "$(config_get_default KERNEL "")" = "AUTO" ]; then
+ local kernel="$(config_get_default DESTDIR "")/netbsd"
+ if [ -e "${kernel}" ]; then
+ local kernel_name="$(config -x "${kernel}" | head -n 1 \
+ | cut -d \" -f 2)"
+ [ -n "${kernel_name}" ] || utils_error "Failed to determine" \
+ "kernel name; please set KERNEL explicitly"
+ config_set KERNEL "${kernel_name}"
+ else
+ config_unset KERNEL
+ fi
+ fi
+
+ if [ "$(config_get_default SETS "")" = "AUTO" ]; then
+ local mtree="$(config_get_default DESTDIR "")/etc/mtree/"
+ if [ -d "${mtree}" ]; then
+ local all_sets="$(cd "${mtree}" && echo set.* \
+ | sed -e 's,set\.,,g')"
+ config_set SETS "${all_sets}"
+ else
+ config_unset SETS
+ fi
+ fi
+}
+
+
+# Dumps the loaded configuration.
+#
+# \params ... The options and arguments to the command.
+sysupgrade_config() {
+ local eval_auto=no
+ while getopts ':a' arg "${@}"; do
+ case "${arg}" in
+ a) # Evaluate automatic settings.
+ eval_auto=yes
+ ;;
+
+ \?)
+ utils_usage_error "Unknown option -${OPTARG}"
+ ;;
+ esac
+ done
+ shift $((${OPTIND} - 1))
+
+ [ ${#} -eq 0 ] || utils_usage_error "config does not take any arguments"
+
+ [ "${eval_auto}" = no ] || sysupgrade_auto_config
+
+ for var in ${SYSUPGRADE_CONFIG_VARS}; do
+ if config_has "${var}"; then
+ echo "${var} = $(config_get "${var}")"
+ else
+ echo "${var} is undefined"
+ fi
+ done
+}
+
+
+# Gets the path to a set, and ensures it exists.
+#
+# \post The path to the tgz of the set is printed on stdout.
+#
+# \param set_name The name of the set to query.
+get_set() {
+ local set_name="${1}"; shift
+
+ echo "$(config_get CACHEDIR)/${set_name}.tgz"
+}
+
+
+# Ensures that a given set exists.
+#
+# \param set_name The name of the set to query.
+require_set() {
+ local set_name="${1}"; shift
+
+ local set_tgz="$(get_set "${set_name}")"
+ [ -f "${set_tgz}" ] || utils_error "Cannot find ${set_name}; did you run" \
+ "'${Utils_ProgName} fetch' first?"
+}
+
+
+# Extracts a set into the destdir.
+#
+# The set to be extracted must have been previously fetched into the cache
+# directory by sysupgrade_fetch command.
+#
+# \param set_name Name of the set to extract, without the .tgz extension.
+extract_set() {
+ local set_name="${1}"; shift
+
+ require_set "${set_name}"
+
+ local destdir="$(config_get_default DESTDIR "")"
+ local set_tgz="$(get_set "${set_name}")"
+
+ utils_info "Extracting ${set_name} into ${destdir}/"
+ [ -z "${destdir}" ] || utils_run mkdir -p "${destdir}"
+ progress -zf "${set_tgz}" tar -xp -C "${destdir}/" -f -
+}
+
+
+# Fetches the release sets into the cache directory.
+#
+# \param releasedir Optional override of the release directory to use.
+sysupgrade_fetch() {
+ [ ${#} -lt 2 ] \
+ || utils_usage_error "fetch takes zero or one arguments"
+
+ [ -z "${1}" ] || config_set "RELEASEDIR" "${1}"
+
+ local releasedir="$(config_get RELEASEDIR)"
+ local cachedir="$(config_get CACHEDIR)"
+ local fetch_sets="$(config_get SETS)"
+ if config_has KERNEL; then
+ fetch_sets="${fetch_sets} kern-$(config_get KERNEL)"
+ fi
+
+ case "${releasedir}" in
+ ftp://*|http://*)
+ mkdir -p "${cachedir}"
+
+ for set_name in ${fetch_sets}; do
+ local file="${cachedir}/${set_name}.tgz"
+ if [ -f "${file}" ]; then
+ utils_warning "Reusing existing ${file}"
+ else
+ local url="${releasedir}/binary/sets/${set_name}.tgz"
+ utils_info "Downloading ${url} into ${cachedir}"
+ rm -f "${file}"
+ ftp -R -o"${file}.tmp" "${url}" \
+ || utils_error "Failed to fetch ${url}"
+ mv "${file}.tmp" "${file}"
+ fi
+ done
+ ;;
+
+ /*)
+ mkdir -p "${cachedir}"
+
+ for set_name in ${fetch_sets}; do
+ local src="${releasedir}/binary/sets/${set_name}.tgz"
+ utils_info "Linking local ${src} into ${cachedir}"
+ [ -f "${src}" ] || utils_error "Cannot open ${src}"
+ ln -s -f "${src}" "${cachedir}/${set_name}.tgz" \
+ || utils_error "Failed to link ${src} into ${cachedir}"
+ done
+ ;;
+
+ *)
+ utils_error "Don't know how to fetch from ${releasedir}; must" \
+ "be an absolute path or an FTP/HTTP site"
+ ;;
+ esac
+}
+
+
+# Installs a new kernel from a set.
+#
+# \param kernel_name Name of the kernel set to use; optional.
+sysupgrade_kernel() {
+ [ ${#} -lt 2 ] \
+ || utils_usage_error "kernel takes zero or one arguments"
+
+ local kernel_name
+ if [ -n "${1}" ]; then
+ kernel_name="${1}"
+ elif config_has KERNEL; then
+ kernel_name="$(config_get KERNEL)"
+ else
+ utils_info "Skipping kernel installation (KERNEL not set)"
+ return 0
+ fi
+
+ require_set "kern-${kernel_name}"
+
+ local destdir="$(config_get_default DESTDIR "")"
+ utils_info "Upgrading kernel using ${kernel_name} in ${destdir}/"
+
+ if [ -f "${destdir}/netbsd" ]; then
+ utils_info "Backing up 'netbsd' kernel as 'onetbsd'"
+ cp "${destdir}/netbsd" "${destdir}/onetbsd"
+ fi
+ extract_set "kern-${kernel_name}"
+}
+
+
+# Installs new kernel modules.
+sysupgrade_modules() {
+ [ ${#} -eq 0 ] \
+ || utils_usage_error "modules does not take any arguments"
+
+ if ! utils_contains modules $(config_get SETS); then
+ utils_info "Skipping modules installation (modules not in SETS)"
+ return 0
+ fi
+
+ utils_info "Upgrading kernel modules"
+
+ extract_set modules
+}
+
+
+# Installs new sets.
+#
+# \param ... Names of the sets to extract, to override SETS.
+sysupgrade_sets() {
+ utils_info "Upgrading base system"
+
+ local sets=
+ for set_name in "${@:-$(config_get SETS)}"; do
+ case "${set_name}" in
+ *etc) ;; # Handled by etcupdate.
+ kern-*) ;; # Handled by kernel.
+ modules) ;; # Handled by modules.
+ *) sets="${sets} ${set_name}" ;;
+ esac
+ done
+
+ for set_name in ${sets}; do
+ require_set "${set_name}"
+ done
+
+ for set_name in ${sets}; do
+ extract_set "${set_name}"
+ done
+}
+
+
+# Runs etcupdate to install new configuration files.
+sysupgrade_etcupdate() {
+ [ ${#} -eq 0 ] || utils_usage_error "etcupdate does not take any arguments"
+
+ if config_has DESTDIR; then
+ utils_info "Skipping etcupdate (DESTDIR upgrades not supported)"
+ return 0
+ fi
+
+ local sets="$(utils_filter '*etc' $(config_get SETS))"
+ if [ -z "${sets}" ]; then
+ utils_info "Skipping etcupdate (no etc sets in SETS)"
+ return 0
+ fi
+ if ! utils_contains etc ${sets}; then
+ utils_info "Skipping etcupdate (required etc not in SETS)"
+ return 0
+ fi
+
+ local sflags=
+ for set_name in ${sets}; do
+ require_set "${set_name}"
+ sflags="${sflags} -s$(get_set "${set_name}")"
+ done
+
+ utils_info "Upgrading /etc interactively"
+ etcupdate -a -l ${sflags}
+}
+
+
+# Runs postinstall to validate the updated system.
+#
+# \param ... Arguments to pass to postinstall(8).
+sysupgrade_postinstall() {
+ local sets=
+ local sets="$(utils_filter '*etc' $(config_get SETS))"
+ if [ -z "${sets}" ]; then
+ utils_info "Skipping postinstall (no etc sets in SETS)"
+ return 0
+ fi
+ if ! utils_contains etc ${sets}; then
+ utils_info "Skipping postinstall (required etc not in SETS)"
+ return 0
+ fi
+
+ local sflags=
+ for set_name in ${sets}; do
+ require_set "${set_name}"
+ sflags="${sflags} -s$(get_set "${set_name}")"
+ done
+
+ utils_info "Performing postinstall checks"
+ local destdir="$(config_get_default DESTDIR "")"
+ if config_has POSTINSTALL_AUTOFIX; then
+ postinstall "-d${destdir}/" ${sflags} fix \
+ $(config_get POSTINSTALL_AUTOFIX)
+ fi
+ postinstall "-d${destdir}/" ${sflags} "${@:-check}" \
+ || utils_error "Some postinstall(8) checks have failed"
+}
+
+
+# Cleans up the cache directory.
+sysupgrade_clean() {
+ [ ${#} -eq 0 ] || utils_usage_error "clean does not take any arguments"
+
+ utils_info "Cleaning downloaded files"
+ rm -f "$(config_get CACHEDIR)"/*.tgz*
+}
+
+
+# Automated upgrade procedure.
+#
+# This is just a convenience mechanism to execute all the different steps of the
+# upgrade.
+#
+# \param releasedir Optional override of the release directory to use.
+sysupgrade_auto() {
+ [ ${#} -lt 2 ] \
+ || utils_usage_error "auto takes zero or one arguments"
+
+ [ -z "${1}" ] || config_set "RELEASEDIR" "${1}"
+
+ local stages=
+ stages="fetch modules kernel sets"
+ config_get_bool "ETCUPDATE" && stages="${stages} etcupdate"
+ stages="${stages} postinstall"
+ config_get_bool "AUTOCLEAN" && stages="${stages} clean"
+
+ utils_info "Starting auto-update with stages: ${stages}"
+ for stage in ${stages}; do
+ sysupgrade_${stage}
+ done
+
+ config_get_bool "AUTOCLEAN" || utils_info "Distribution sets not deleted;" \
+ "further ${Utils_ProgName} commands will reuse them"
+}
+
+
+# Entry point to the program.
+#
+# \param ... Command-line arguments to be processed.
+#
+# \return An exit code to be returned to the user.
+sysupgrade_main() {
+ local config_file="${SYSUPGRADE_ETCDIR}/sysupgrade.conf"
+
+ config_init ${SYSUPGRADE_CONFIG_VARS}
+
+ while getopts ':c:d:o:' arg "${@}"; do
+ case "${arg}" in
+ c) # Path to the configuration file.
+ config_file="${OPTARG}"
+ ;;
+
+ d) # Path to the destdir.
+ config_set DESTDIR "${OPTARG}"
+ ;;
+
+ o) # Override for a particular configuration variable.
+ config_override "${OPTARG}"
+ ;;
+
+ \?)
+ utils_usage_error "Unknown option -${OPTARG}"
+ ;;
+ esac
+ done
+ shift $((${OPTIND} - 1))
+
+ [ ${#} -ge 1 ] || utils_usage_error "No command specified"
+
+ local exit_code=0
+
+ local command="${1}"; shift
+ case "${command}" in
+ auto|clean|etcupdate|fetch|kernel|modules|sets|postinstall)
+ sysupgrade_set_defaults
+ config_load "${config_file}"
+ sysupgrade_auto_config
+ "sysupgrade_$(echo "${command}" | tr - _)" "${@}" \
+ || exit_code="${?}"
+ ;;
+
+ config)
+ sysupgrade_set_defaults
+ config_load "${config_file}"
+ "sysupgrade_$(echo "${command}" | tr - _)" "${@}" \
+ || exit_code="${?}"
+ ;;
+
+ *)
+ utils_usage_error "Unknown command ${command}"
+ ;;
+ esac
+
+ return "${exit_code}"
+}
diff --git a/sysutils/sysupgrade/files/sysupgrade_test.sh b/sysutils/sysupgrade/files/sysupgrade_test.sh
new file mode 100644
index 00000000000..6c9b2b1dc88
--- /dev/null
+++ b/sysutils/sysupgrade/files/sysupgrade_test.sh
@@ -0,0 +1,1055 @@
+# Copyright 2012 Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * 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.
+# * Neither the name of Google Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
+# OWNER 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.
+
+
+# Creates a fake program that records its invocations for later processing.
+#
+# The fake program, when invoked, will append its arguments to a commands.log
+# file in the test case's work directory.
+#
+# \param binary The path to the program to create.
+# \param delegate If set to 'yes', execute the real program afterwards.
+create_mock_binary() {
+ local binary="${1}"; shift
+ local delegate=no
+ [ ${#} -eq 0 ] || { delegate="${1}"; shift; }
+
+ cat >"${binary}" <<EOF
+#! /bin/sh
+
+logfile="${HOME}/commands.log"
+echo "Command: \${0##*/}" >>"\${logfile}"
+echo "Directory: \$(pwd)" >>"\${logfile}"
+for arg in "\${@}"; do
+ echo "Arg: \${arg}" >>"\${logfile}"
+done
+ echo >>"\${logfile}"
+EOF
+
+ if [ "${delegate}" = yes ]; then
+ cat >>"${binary}" <<EOF
+PATH="${PATH}"
+exec "\${0##*/}" "\${@}"
+EOF
+ fi
+
+ chmod +x "${binary}"
+}
+
+
+# Generates a NetBSD release dir with fake tarballs.
+#
+# Each of the generated sets contains a single file named '<set>.cookie'.
+# Additionally, if the set starts with kern-, a 'netbsd' file will also be
+# inside it.
+#
+# \param releasedir Path to the release directory.
+# \param ... Names of the sets to create under releasedir/binary/sets/.
+create_mock_release() {
+ local releasedir="${1}"; shift
+
+ mkdir -p "${releasedir}/binary/sets"
+
+ for set_name in "${@}"; do
+ local files=
+
+ echo "File from ${set_name}" >"${set_name}.cookie"
+ files="${files} ${set_name}.cookie"
+
+ case "${set_name}" in
+ kern-*)
+ echo "File from ${set_name}" >netbsd
+ files="${files} netbsd"
+ ;;
+ esac
+
+ tar czf "${releasedir}/binary/sets/${set_name}.tgz" ${files}
+ rm ${files}
+ done
+}
+
+
+atf_test_case config__builtins
+config__builtins_body() {
+ cat >expout <<EOF
+AUTOCLEAN = yes
+CACHEDIR = @SYSUPGRADE_CACHEDIR@
+DESTDIR is undefined
+ETCUPDATE = yes
+KERNEL = AUTO
+POSTINSTALL_AUTOFIX is undefined
+RELEASEDIR is undefined
+SETS = AUTO
+EOF
+ atf_check -o file:expout sysupgrade -c /dev/null config
+}
+
+
+atf_test_case config__default_file
+config__default_file_body() {
+ mkdir system
+ export SYSUPGRADE_ETCDIR="$(pwd)/system"
+
+ echo "KERNEL=abc" >"system/sysupgrade.conf"
+ atf_check -o match:"KERNEL = abc" sysupgrade config
+}
+
+
+atf_test_case config__explicit_file
+config__explicit_file_body() {
+ mkdir system
+ export SYSUPGRADE_ETCDIR="$(pwd)/system"
+
+ echo "KERNEL=abc" >"system/sysupgrade.conf"
+ echo "SETS='d e'" >"my-file.conf"
+ atf_check -o not-match:"KERNEL = abc" -o match:"SETS = d e" \
+ sysupgrade -c ./my-file.conf config
+}
+
+
+atf_test_case config__auto__kernel__found
+config__auto__kernel__found_body() {
+ cat >config <<EOF
+#! /bin/sh
+if [ "\${1}" != "-x" ]; then
+ echo "-x not passed to config"
+ exit 1
+fi
+if [ "\${2}" != "$(pwd)/root/netbsd" ]; then
+ echo "Invalid path passed to config: \${2}"
+ exit 1
+fi
+
+cat "\${2}"
+EOF
+ chmod +x config
+ PATH="$(pwd):${PATH}"
+
+ mkdir root
+ cat >root/netbsd <<EOF
+### START CONFIG FILE "ABCDE"
+these are some contents
+### END CONFIG FILE "ABCDE"
+EOF
+ atf_check -o match:"KERNEL = ABCDE" \
+ sysupgrade -c /dev/null \
+ -o DESTDIR="$(pwd)/root" -o KERNEL="AUTO" config -a
+}
+
+
+atf_test_case config__auto__kernel__not_found
+config__auto__kernel__not_found_body() {
+
+ atf_check -o match:"KERNEL is undefined" \
+ sysupgrade -c /dev/null \
+ -o DESTDIR="$(pwd)/root" -o KERNEL="AUTO" config -a
+}
+
+
+atf_test_case config__auto__kernel__fail
+config__auto__kernel__fail_body() {
+ cat >config <<EOF
+#! /bin/sh
+exit 1
+EOF
+ chmod +x config
+ PATH="$(pwd):${PATH}"
+
+ mkdir root
+ touch root/netbsd
+
+ cat >experr <<EOF
+sysupgrade: E: Failed to determine kernel name; please set KERNEL explicitly
+EOF
+ atf_check -s exit:1 -e file:experr sysupgrade -c /dev/null \
+ -o DESTDIR="$(pwd)/root" -o KERNEL="AUTO" config -a
+}
+
+
+atf_test_case config__auto__sets__found
+config__auto__sets__found_body() {
+ mkdir -p root/etc/mtree
+ touch root/etc/mtree/set.first
+ touch root/etc/mtree/set.second
+ touch root/etc/mtree/set.third
+ atf_check -o match:"SETS = first second third" \
+ sysupgrade -c /dev/null \
+ -o DESTDIR="$(pwd)/root" -o SETS="AUTO" config -a
+}
+
+
+atf_test_case config__auto__sets__not_found
+config__auto__sets__not_found_body() {
+
+ atf_check -o match:"SETS is undefined" \
+ sysupgrade -c /dev/null \
+ -o DESTDIR="$(pwd)/root" -o SETS="AUTO" config -a
+}
+
+
+atf_test_case config__not_found
+config__not_found_body() {
+ mkdir .sysupgrade
+ mkdir system
+ export SYSUPGRADE_ETCDIR="$(pwd)/system"
+
+ cat >experr <<EOF
+sysupgrade: E: Configuration file foobar does not exist
+EOF
+ atf_check -s exit:1 -o empty -e file:experr sysupgrade -c foobar config
+}
+
+
+atf_test_case config__overrides
+config__overrides_body() {
+ cat >custom.conf <<EOF
+CACHEDIR=/tmp/cache
+KERNEL=foo
+EOF
+
+ cat >expout <<EOF
+AUTOCLEAN = yes
+CACHEDIR = /tmp/cache2
+DESTDIR = /tmp/destdir
+ETCUPDATE = yes
+KERNEL is undefined
+POSTINSTALL_AUTOFIX is undefined
+RELEASEDIR is undefined
+SETS = AUTO
+EOF
+ atf_check -o file:expout sysupgrade -c custom.conf -o KERNEL= \
+ -o CACHEDIR=/tmp/cache2 -o DESTDIR=/tmp/destdir config
+}
+
+
+atf_test_case config__too_many_args
+config__too_many_args_body() {
+ cat >experr <<EOF
+sysupgrade: E: config does not take any arguments
+Type 'man sysupgrade' for help
+EOF
+ atf_check -s exit:1 -e file:experr sysupgrade -c /dev/null config foo
+}
+
+
+atf_test_case fetch__ftp
+fetch__ftp_body() {
+ # TODO(jmmv): It would be nice if this test used an actual FTP server, just
+ # like the http test below. Unfortunately, the ftpd shipped by NetBSD does
+ # not provide an easy mechanism to run it as non-root (e.g. no easy way to
+ # select on which port to serve).
+ create_mock_binary ftp
+cat >>ftp <<EOF
+for arg in "\${@}"; do
+ case "\${arg}" in
+ -o*) touch \$(echo "\${arg}" | sed -e s,^-o,,) ;;
+ esac
+done
+EOF
+ PATH="$(pwd):${PATH}"
+
+ SYSUPGRADE_CACHEDIR="$(pwd)/a/b/c"; export SYSUPGRADE_CACHEDIR
+ atf_check -o ignore -e ignore sysupgrade -c /dev/null \
+ -o RELEASEDIR="ftp://example.net/pub/NetBSD/X.Y/a-machine" \
+ -o SETS="a foo" fetch
+
+ cat >expout <<EOF
+Command: ftp
+Directory: $(pwd)
+Arg: -R
+Arg: -o$(pwd)/a/b/c/a.tgz.tmp
+Arg: ftp://example.net/pub/NetBSD/X.Y/a-machine/binary/sets/a.tgz
+
+Command: ftp
+Directory: $(pwd)
+Arg: -R
+Arg: -o$(pwd)/a/b/c/foo.tgz.tmp
+Arg: ftp://example.net/pub/NetBSD/X.Y/a-machine/binary/sets/foo.tgz
+
+Command: ftp
+Directory: $(pwd)
+Arg: -R
+Arg: -o$(pwd)/a/b/c/kern-GENERIC.tgz.tmp
+Arg: ftp://example.net/pub/NetBSD/X.Y/a-machine/binary/sets/kern-GENERIC.tgz
+
+EOF
+ atf_check -o file:expout cat commands.log
+}
+
+
+atf_test_case fetch__http cleanup
+fetch__http_head() {
+ atf_set "require.progs" "/usr/libexec/httpd"
+}
+fetch__http_body() {
+ create_mock_release www/a-machine base comp etc kern-GENERIC tests text
+
+ /usr/libexec/httpd -b -s -d -I 30401 -P "$(pwd)/httpd.pid" "$(pwd)/www" \
+ || atf_fail "Failed to start test HTTP server"
+
+ SYSUPGRADE_CACHEDIR="$(pwd)/cache"; export SYSUPGRADE_CACHEDIR
+ atf_check -o ignore -e ignore sysupgrade -c /dev/null \
+ -o RELEASEDIR="http://localhost:30401/a-machine" \
+ -o SETS="base etc text" fetch
+
+ for set_name in base etc kern-GENERIC text; do
+ [ -e "cache/${set_name}.tgz" ] || atf_fail "${set_name} not fetched"
+ done
+ for set_name in comp tests; do
+ [ ! -e "cache/${set_name}.tgz" ] || atf_fail "${set_name} fetched"
+ done
+
+ kill -9 "$(cat httpd.pid)"
+ rm -f httpd.pid
+}
+fetch__http_cleanup() {
+ if [ -f httpd.pid ]; then
+ echo "Killing stale HTTP server"
+ kill -9 "$(cat httpd.pid)"
+ fi
+}
+
+
+atf_test_case fetch__local
+fetch__local_body() {
+ create_mock_release release base comp etc kern-GENERIC tests text
+
+ SYSUPGRADE_CACHEDIR="$(pwd)/cache"; export SYSUPGRADE_CACHEDIR
+ atf_check -o ignore -e ignore sysupgrade -c /dev/null \
+ -o RELEASEDIR="$(pwd)/release" -o SETS="base etc text" fetch
+
+ for set_name in base etc kern-GENERIC text; do
+ [ -e "cache/${set_name}.tgz" ] || atf_fail "${set_name} not fetched"
+ done
+ for set_name in comp tests; do
+ [ ! -e "cache/${set_name}.tgz" ] || atf_fail "${set_name} fetched"
+ done
+}
+
+
+atf_test_case fetch__no_kernel
+fetch__no_kernel_body() {
+ create_mock_release release base comp kern-GENERIC
+
+ SYSUPGRADE_CACHEDIR="$(pwd)/cache"; export SYSUPGRADE_CACHEDIR
+ atf_check -o ignore -e ignore sysupgrade -c /dev/null \
+ -o KERNEL= -o RELEASEDIR="$(pwd)/release" -o SETS="base comp" fetch
+
+ for set_name in base comp; do
+ [ -e "cache/${set_name}.tgz" ] || atf_fail "${set_name} not fetched"
+ done
+ [ ! -e "cache/kern-GENERIC.tgz" ] || atf_fail "kern-GENERIC fetched"
+}
+
+
+atf_test_case fetch__unknown
+fetch__unknown_body() {
+ cat >experr <<EOF
+sysupgrade: E: Don't know how to fetch from ./release; must be an absolute path or an FTP/HTTP site
+EOF
+
+ SYSUPGRADE_CACHEDIR="$(pwd)/cache"; export SYSUPGRADE_CACHEDIR
+ atf_check -s exit:1 -e file:experr sysupgrade -c /dev/null \
+ -o RELEASEDIR="./release" -o SETS="base etc text" fetch
+ [ ! -d cache ] || atf_fail "cache directory created even during errors"
+}
+
+
+atf_test_case fetch__explicit
+fetch__explicit_body() {
+ create_mock_release release base
+
+ SYSUPGRADE_CACHEDIR="$(pwd)/cache"; export SYSUPGRADE_CACHEDIR
+ atf_check -o ignore -e ignore sysupgrade -c /dev/null \
+ -o RELEASEDIR="$(pwd)/foo" -o KERNEL= -o SETS=base \
+ fetch "$(pwd)/release"
+
+ [ -e "cache/base.tgz" ] || atf_fail "base not fetched"
+}
+
+
+atf_test_case fetch__too_many_args
+fetch__too_many_args_body() {
+ cat >experr <<EOF
+sysupgrade: E: fetch takes zero or one arguments
+Type 'man sysupgrade' for help
+EOF
+ atf_check -s exit:1 -e file:experr sysupgrade -c /dev/null fetch foo bar
+}
+
+
+atf_test_case kernel__skip
+kernel__skip_body() {
+ cat >experr <<EOF
+sysupgrade: I: Skipping kernel installation (KERNEL not set)
+EOF
+ atf_check -s exit:0 -e file:experr sysupgrade -c /dev/null -o KERNEL= kernel
+}
+
+
+atf_test_case kernel__from_config
+kernel__from_config_body() {
+ mkdir root
+ echo "my old kernel" >root/netbsd
+
+ create_mock_release release kern-FOOBAR
+ SYSUPGRADE_CACHEDIR="$(pwd)/release/binary/sets"; export SYSUPGRADE_CACHEDIR
+
+ atf_check -s exit:0 \
+ -e match:"Upgrading kernel using FOOBAR in $(pwd)/root/" \
+ -e match:"Backing up" \
+ sysupgrade -c /dev/null \
+ -o DESTDIR="$(pwd)/root" -o KERNEL=FOOBAR kernel
+
+ atf_check -o match:"File from kern-FOOBAR" cat root/netbsd
+ atf_check -o match:"my old kernel" cat root/onetbsd
+}
+
+
+atf_test_case kernel__from_arg
+kernel__from_arg_body() {
+ mkdir root
+ echo "my old kernel" >root/netbsd
+
+ create_mock_release release kern-FOOBAR kern-OTHER
+ SYSUPGRADE_CACHEDIR="$(pwd)/release/binary/sets"; export SYSUPGRADE_CACHEDIR
+
+ atf_check -s exit:0 \
+ -e match:"Upgrading kernel using OTHER in $(pwd)/root/" \
+ -e match:"Backing up" \
+ sysupgrade -c /dev/null \
+ -o DESTDIR="$(pwd)/root" -o KERNEL=FOOBAR kernel OTHER
+
+ atf_check -o match:"File from kern-OTHER" cat root/netbsd
+ atf_check -o match:"my old kernel" cat root/onetbsd
+}
+
+
+atf_test_case kernel__no_backup
+kernel__no_backup_body() {
+ mkdir root
+
+ create_mock_release release kern-FOOBAR
+ SYSUPGRADE_CACHEDIR="$(pwd)/release/binary/sets"; export SYSUPGRADE_CACHEDIR
+
+ atf_check -s exit:0 -e not-match:"Backing up" sysupgrade -c /dev/null \
+ -o DESTDIR="$(pwd)/root" -o KERNEL=FOOBAR kernel
+
+ atf_check -o match:"File from kern-FOOBAR" cat root/netbsd
+ [ ! -f root/onetbsd ] || "onetbsd backup created, but not expected"
+}
+
+
+atf_test_case kernel__missing_set
+kernel__missing_set_body() {
+ mkdir root
+ echo "my old kernel" >root/netbsd
+
+ SYSUPGRADE_CACHEDIR="$(pwd)"; export SYSUPGRADE_CACHEDIR
+
+ cat >experr <<EOF
+sysupgrade: E: Cannot find kern-GENERIC; did you run 'sysupgrade fetch' first?
+EOF
+ atf_check -s exit:1 -e file:experr sysupgrade -c /dev/null \
+ -o DESTDIR="$(pwd)/root" -o KERNEL=GENERIC kernel
+
+ atf_check -o match:"my old kernel" cat root/netbsd
+}
+
+
+atf_test_case kernel__too_many_args
+kernel__too_many_args_body() {
+ cat >experr <<EOF
+sysupgrade: E: kernel takes zero or one arguments
+Type 'man sysupgrade' for help
+EOF
+ atf_check -s exit:1 -e file:experr sysupgrade -c /dev/null kernel A B
+}
+
+
+atf_test_case modules__skip
+modules__skip_body() {
+ cat >experr <<EOF
+sysupgrade: I: Skipping modules installation (modules not in SETS)
+EOF
+ atf_check -s exit:0 -e file:experr sysupgrade -c /dev/null \
+ -o SETS="base comp modules2" modules
+}
+
+
+atf_test_case modules__install
+modules__install_body() {
+ create_mock_release release base modules
+ SYSUPGRADE_CACHEDIR="$(pwd)/release/binary/sets"; export SYSUPGRADE_CACHEDIR
+
+ atf_check -s exit:0 -e match:"Upgrading kernel modules" \
+ sysupgrade -c /dev/null -o DESTDIR="$(pwd)/root" \
+ -o SETS="base modules tests" modules
+
+ [ -f root/modules.cookie ] || atf_fail "modules not extracted"
+ [ ! -f root/base.cookie ] || atf_fail "base should not have been extracted"
+}
+
+
+atf_test_case modules__too_many_args
+modules__too_many_args_body() {
+ cat >experr <<EOF
+sysupgrade: E: modules does not take any arguments
+Type 'man sysupgrade' for help
+EOF
+ atf_check -s exit:1 -e file:experr sysupgrade -c /dev/null modules A
+}
+
+
+atf_test_case sets__from_config
+sets__from_config_body() {
+ create_mock_release release base etc comp
+ SYSUPGRADE_CACHEDIR="$(pwd)/release/binary/sets"; export SYSUPGRADE_CACHEDIR
+
+ expected_sets="base comp"
+ unexpected_sets="etc kern-GENERIC modules xetc"
+
+ atf_check -s exit:0 -e ignore sysupgrade -c /dev/null \
+ -o DESTDIR="$(pwd)/root" \
+ -o SETS="${expected_sets} ${unexpected_sets}" \
+ sets
+
+ for set_name in ${expected_sets}; do
+ [ -f "root/${set_name}.cookie" ] \
+ || atf_fail "Expected set ${set_name} not extracted"
+ done
+
+ for set_name in ${unexpected_sets}; do
+ [ ! -f "root/${set_name}.cookie" ] \
+ || atf_fail "Unexpected set ${set_name} extracted"
+ done
+}
+
+
+atf_test_case sets__from_args
+sets__from_args_body() {
+ create_mock_release release base etc comp
+ SYSUPGRADE_CACHEDIR="$(pwd)/release/binary/sets"; export SYSUPGRADE_CACHEDIR
+
+ expected_sets="base comp"
+ unexpected_sets="etc kern-GENERIC modules xetc"
+
+ atf_check -s exit:0 -e ignore sysupgrade -c /dev/null \
+ -o DESTDIR="$(pwd)/root" -o SETS="foo bar baz" \
+ sets ${expected_sets} ${unexpected_sets}
+
+ for set_name in ${expected_sets}; do
+ [ -f "root/${set_name}.cookie" ] \
+ || atf_fail "Expected set ${set_name} not extracted"
+ done
+
+ for set_name in ${unexpected_sets}; do
+ [ ! -f "root/${set_name}.cookie" ] \
+ || atf_fail "Unexpected set ${set_name} extracted"
+ done
+}
+
+
+atf_test_case etcupdate__skip__none
+etcupdate__skip__none_body() {
+ cat >experr <<EOF
+sysupgrade: I: Skipping etcupdate (no etc sets in SETS)
+EOF
+ atf_check -s exit:0 -e file:experr sysupgrade -c /dev/null \
+ -o SETS="base comp etcfoo" etcupdate
+}
+
+
+atf_test_case etcupdate__skip__no_etc
+etcupdate__skip__no_etc_body() {
+ cat >experr <<EOF
+sysupgrade: I: Skipping etcupdate (required etc not in SETS)
+EOF
+ atf_check -s exit:0 -e file:experr sysupgrade -c /dev/null \
+ -o SETS="base xetc xfonts" etcupdate
+}
+
+
+atf_test_case etcupdate__skip__destdir
+etcupdate__skip__destdir_body() {
+ cat >experr <<EOF
+sysupgrade: I: Skipping etcupdate (DESTDIR upgrades not supported)
+EOF
+ atf_check -s exit:0 -e file:experr sysupgrade -c /dev/null \
+ -o DESTDIR="$(pwd)" -o SETS="etc" etcupdate
+}
+
+
+atf_test_case etcupdate__some_etcs
+etcupdate__some_etcs_body() {
+ create_mock_binary etcupdate
+ PATH="$(pwd):${PATH}"
+
+ create_mock_release release base etc comp xbase xetc
+ SYSUPGRADE_CACHEDIR="$(pwd)/release/binary/sets"; export SYSUPGRADE_CACHEDIR
+
+ atf_check -s exit:0 -o ignore -e ignore sysupgrade -c /dev/null \
+ -o RELEASEDIR="$(pwd)/release" \
+ -o SETS="base etc comp xbase xetc" etcupdate
+
+ cat >expout <<EOF
+Command: etcupdate
+Directory: $(pwd)
+Arg: -a
+Arg: -l
+Arg: -s$(pwd)/release/binary/sets/etc.tgz
+Arg: -s$(pwd)/release/binary/sets/xetc.tgz
+
+EOF
+ atf_check -o file:expout cat commands.log
+}
+
+
+atf_test_case etcupdate__missing_etc
+etcupdate__missing_etc_body() {
+ cat >experr <<EOF
+sysupgrade: E: Cannot find etc; did you run 'sysupgrade fetch' first?
+EOF
+ atf_check -s exit:1 -e file:experr sysupgrade -c /dev/null \
+ -o RELEASEDIR="$(pwd)/missing" -o SETS="etc" etcupdate
+}
+
+
+atf_test_case etcupdate__too_many_args
+etcupdate__too_many_args_body() {
+ cat >experr <<EOF
+sysupgrade: E: etcupdate does not take any arguments
+Type 'man sysupgrade' for help
+EOF
+ atf_check -s exit:1 -e file:experr sysupgrade -c /dev/null etcupdate foo
+}
+
+
+atf_test_case postinstall__skip__none
+postinstall__skip__none_body() {
+ cat >experr <<EOF
+sysupgrade: I: Skipping postinstall (no etc sets in SETS)
+EOF
+ atf_check -s exit:0 -e file:experr sysupgrade -c /dev/null \
+ -o SETS="base comp etcfoo" postinstall
+}
+
+
+atf_test_case postinstall__skip__no_etc
+postinstall__skip__no_etc_body() {
+ cat >experr <<EOF
+sysupgrade: I: Skipping postinstall (required etc not in SETS)
+EOF
+ atf_check -s exit:0 -e file:experr sysupgrade -c /dev/null \
+ -o SETS="base xetc xfonts" postinstall
+}
+
+
+atf_test_case postinstall__some_etcs
+postinstall__some_etcs_body() {
+ create_mock_binary postinstall
+ PATH="$(pwd):${PATH}"
+
+ create_mock_release release base etc comp xbase xetc
+ SYSUPGRADE_CACHEDIR="$(pwd)/release/binary/sets"; export SYSUPGRADE_CACHEDIR
+
+ atf_check -s exit:0 -o ignore -e ignore sysupgrade -c /dev/null \
+ -o RELEASEDIR="$(pwd)/release" \
+ -o SETS="base etc comp xbase xetc" postinstall
+
+ cat >expout <<EOF
+Command: postinstall
+Directory: $(pwd)
+Arg: -d/
+Arg: -s$(pwd)/release/binary/sets/etc.tgz
+Arg: -s$(pwd)/release/binary/sets/xetc.tgz
+Arg: check
+
+EOF
+ atf_check -o file:expout cat commands.log
+}
+
+
+atf_test_case postinstall__destdir
+postinstall__destdir_body() {
+ create_mock_binary postinstall
+ PATH="$(pwd):${PATH}"
+
+ create_mock_release release base etc
+ SYSUPGRADE_CACHEDIR="$(pwd)/release/binary/sets"; export SYSUPGRADE_CACHEDIR
+
+ atf_check -s exit:0 -o ignore -e ignore sysupgrade -c /dev/null \
+ -o DESTDIR="$(pwd)/root" \
+ -o RELEASEDIR="$(pwd)/release" \
+ -o SETS="base etc" postinstall
+
+ cat >expout <<EOF
+Command: postinstall
+Directory: $(pwd)
+Arg: -d$(pwd)/root/
+Arg: -s$(pwd)/release/binary/sets/etc.tgz
+Arg: check
+
+EOF
+ atf_check -o file:expout cat commands.log
+}
+
+
+atf_test_case postinstall__autofix
+postinstall__autofix_body() {
+ create_mock_binary postinstall
+ PATH="$(pwd):${PATH}"
+
+ create_mock_release release etc
+ SYSUPGRADE_CACHEDIR="$(pwd)/release/binary/sets"; export SYSUPGRADE_CACHEDIR
+
+ atf_check -s exit:0 -o ignore -e ignore sysupgrade -c /dev/null \
+ -o POSTINSTALL_AUTOFIX="first second" \
+ -o RELEASEDIR="$(pwd)/release" \
+ -o SETS="etc" postinstall
+
+ cat >expout <<EOF
+Command: postinstall
+Directory: $(pwd)
+Arg: -d/
+Arg: -s$(pwd)/release/binary/sets/etc.tgz
+Arg: fix
+Arg: first
+Arg: second
+
+Command: postinstall
+Directory: $(pwd)
+Arg: -d/
+Arg: -s$(pwd)/release/binary/sets/etc.tgz
+Arg: check
+
+EOF
+ atf_check -o file:expout cat commands.log
+}
+
+
+atf_test_case postinstall__missing_etc
+postinstall__missing_etc_body() {
+ cat >experr <<EOF
+sysupgrade: E: Cannot find etc; did you run 'sysupgrade fetch' first?
+EOF
+ atf_check -s exit:1 -e file:experr sysupgrade -c /dev/null \
+ -o RELEASEDIR="$(pwd)/missing" -o SETS="etc" postinstall
+}
+
+
+atf_test_case postinstall__explicit_args
+postinstall__explicit_args_body() {
+ create_mock_binary postinstall
+ PATH="$(pwd):${PATH}"
+
+ create_mock_release release base etc
+ SYSUPGRADE_CACHEDIR="$(pwd)/release/binary/sets"; export SYSUPGRADE_CACHEDIR
+
+ atf_check -s exit:0 -o ignore -e ignore sysupgrade -c /dev/null \
+ -o RELEASEDIR="$(pwd)/release" -o SETS="base etc" postinstall fix a b c
+
+ cat >expout <<EOF
+Command: postinstall
+Directory: $(pwd)
+Arg: -d/
+Arg: -s$(pwd)/release/binary/sets/etc.tgz
+Arg: fix
+Arg: a
+Arg: b
+Arg: c
+
+EOF
+ atf_check -o file:expout cat commands.log
+}
+
+
+atf_test_case clean__nothing
+clean__nothing_body() {
+ SYSUPGRADE_CACHEDIR="$(pwd)/cache"; export SYSUPGRADE_CACHEDIR
+ mkdir "${SYSUPGRADE_CACHEDIR}"
+ atf_check -e match:"Cleaning downloaded files" sysupgrade -c /dev/null clean
+ [ -d cache ] || atf_fail "Cache directory should not have been deleted"
+}
+
+
+atf_test_case clean__only_tgzs
+clean__only_tgzs_body() {
+ SYSUPGRADE_CACHEDIR="$(pwd)/cache"; export SYSUPGRADE_CACHEDIR
+ mkdir "${SYSUPGRADE_CACHEDIR}"
+ touch cache/foo.tgz
+ touch cache/foo.tgz.tmp
+ touch cache/bar.gz
+ atf_check -e match:"Cleaning downloaded files" sysupgrade -c /dev/null clean
+ [ ! -f cache/foo.tgz ] || atf_fail "tgz not deleted"
+ [ ! -f cache/foo.tgz.tmp ] || atf_fail "Temporary tgz not deleted"
+ [ -f cache/bar.gz ] || atf_fail "Non-tgz files deleted"
+ [ -d cache ] || atf_fail "Cache directory should not have been deleted"
+}
+
+
+atf_test_case clean__too_many_args
+clean__too_many_args_body() {
+ cat >experr <<EOF
+sysupgrade: E: clean does not take any arguments
+Type 'man sysupgrade' for help
+EOF
+ atf_check -s exit:1 -e file:experr sysupgrade -c /dev/null clean foo
+}
+
+
+atf_test_case auto__simple
+auto__simple_body() {
+ create_mock_binary postinstall
+ create_mock_binary etcupdate
+ PATH="$(pwd):${PATH}"
+
+ create_mock_release release base comp etc kern-CUSTOM modules
+
+ mkdir root
+ echo "old kernel" >root/netbsd
+
+ cat >sysupgrade.conf <<EOF
+CACHEDIR="$(pwd)/cache"
+KERNEL="CUSTOM"
+RELEASEDIR="$(pwd)/release"
+SETS="base comp etc modules"
+EOF
+
+ atf_check \
+ -e match:"Linking local" \
+ -e match:"Upgrading kernel using" \
+ -e match:"Upgrading kernel modules" \
+ -e match:"Upgrading base system" \
+ -e match:"Skipping etcupdate.*DESTDIR" \
+ -e match:"Performing postinstall checks" \
+ -e match:"Cleaning downloaded files" \
+ sysupgrade -c sysupgrade.conf -d "$(pwd)/root" auto
+
+ atf_check -o inline:"File from base\n" cat root/base.cookie
+ atf_check -o inline:"File from comp\n" cat root/comp.cookie
+ atf_check -o inline:"File from modules\n" cat root/modules.cookie
+ atf_check -o inline:"File from kern-CUSTOM\n" cat root/netbsd
+ atf_check -o inline:"old kernel\n" cat root/onetbsd
+
+ [ ! -f root/etc.cookie ] || atf_fail "etc extracted by mistake"
+
+ [ ! -f cache/base.tgz ] || atf_fail "Cache should have been cleaned"
+}
+
+
+atf_test_case auto__custom_releasedir
+auto__custom_releasedir_body() {
+ create_mock_binary postinstall
+ create_mock_binary etcupdate
+ PATH="$(pwd):${PATH}"
+
+ create_mock_release release2 base etc
+
+ cat >sysupgrade.conf <<EOF
+CACHEDIR="$(pwd)/cache"
+KERNEL=
+RELEASEDIR="$(pwd)/release"
+SETS="base etc"
+EOF
+
+ atf_check \
+ -e match:"Linking local" \
+ -e not-match:"Upgrading kernel using" \
+ -e not-match:"Upgrading kernel modules" \
+ -e match:"Upgrading base system" \
+ -e match:"Skipping etcupdate.*DESTDIR" \
+ -e match:"Performing postinstall checks" \
+ -e match:"Cleaning downloaded files" \
+ sysupgrade -c sysupgrade.conf -d "$(pwd)/root" \
+ auto "$(pwd)/release2"
+
+ atf_check -o inline:"File from base\n" cat root/base.cookie
+
+ [ ! -f root/etc.cookie ] || atf_fail "etc extracted by mistake"
+ [ ! -f root/onetbsd ] || atf_fail "Spurious kernel backup created"
+}
+
+
+atf_test_case auto__skip_etcupdate
+auto__skip_etcupdate_body() {
+ create_mock_binary postinstall
+ PATH="$(pwd):${PATH}"
+
+ create_mock_release release base etc
+
+ cat >sysupgrade.conf <<EOF
+CACHEDIR="$(pwd)/cache"
+ETCUPDATE=no
+RELEASEDIR="$(pwd)/release"
+SETS="base etc"
+EOF
+
+ atf_check -e not-match:"etcupdate" \
+ sysupgrade -c sysupgrade.conf -d "$(pwd)/root" auto
+}
+
+
+atf_test_case auto__skip_clean
+auto__skip_clean_body() {
+ PATH="$(pwd):${PATH}"
+
+ create_mock_release release2 base
+
+ cat >sysupgrade.conf <<EOF
+AUTOCLEAN=no
+CACHEDIR="$(pwd)/cache"
+KERNEL=
+RELEASEDIR="$(pwd)/release"
+SETS="base"
+EOF
+
+ atf_check \
+ -e match:"Linking local" \
+ -e not-match:"Upgrading kernel using" \
+ -e not-match:"Upgrading kernel modules" \
+ -e match:"Upgrading base system" \
+ -e match:"Skipping etcupdate" \
+ -e match:"Skipping postinstall" \
+ -e not-match:"Cleaning downloaded files" \
+ sysupgrade -c sysupgrade.conf -d "$(pwd)/root" \
+ auto "$(pwd)/release2"
+
+ atf_check -o inline:"File from base\n" cat root/base.cookie
+
+ [ ! -f root/etc.cookie ] || atf_fail "etc extracted by mistake"
+ [ ! -f root/onetbsd ] || atf_fail "Spurious kernel backup created"
+
+ [ -f cache/base.tgz ] || atf_fail "Cache should not have been cleaned"
+}
+
+
+atf_test_case auto__too_many_args
+auto__too_many_args_body() {
+ cat >experr <<EOF
+sysupgrade: E: auto takes zero or one arguments
+Type 'man sysupgrade' for help
+EOF
+ atf_check -s exit:1 -e file:experr sysupgrade -c /dev/null auto a b
+}
+
+
+atf_test_case no_command
+no_command_body() {
+ cat >experr <<EOF
+sysupgrade: E: No command specified
+Type 'man sysupgrade' for help
+EOF
+ atf_check -s exit:1 -e file:experr sysupgrade
+}
+
+
+atf_test_case unknown_command
+unknown_command_body() {
+ cat >experr <<EOF
+sysupgrade: E: Unknown command foo
+Type 'man sysupgrade' for help
+EOF
+ atf_check -s exit:1 -e file:experr sysupgrade foo
+}
+
+
+atf_test_case unknown_flag
+unknown_flag_body() {
+ cat >experr <<EOF
+sysupgrade: E: Unknown option -Z
+Type 'man sysupgrade' for help
+EOF
+ atf_check -s exit:1 -e file:experr sysupgrade -Z
+}
+
+
+atf_init_test_cases() {
+ atf_add_test_case config__builtins
+ atf_add_test_case config__default_file
+ atf_add_test_case config__explicit_file
+ atf_add_test_case config__auto__kernel__found
+ atf_add_test_case config__auto__kernel__not_found
+ atf_add_test_case config__auto__kernel__fail
+ atf_add_test_case config__auto__sets__found
+ atf_add_test_case config__auto__sets__not_found
+ atf_add_test_case config__not_found
+ atf_add_test_case config__overrides
+ atf_add_test_case config__too_many_args
+
+ atf_add_test_case fetch__ftp
+ atf_add_test_case fetch__http
+ atf_add_test_case fetch__local
+ atf_add_test_case fetch__no_kernel
+ atf_add_test_case fetch__unknown
+ atf_add_test_case fetch__explicit
+ atf_add_test_case fetch__too_many_args
+
+ atf_add_test_case kernel__skip
+ atf_add_test_case kernel__from_config
+ atf_add_test_case kernel__from_arg
+ atf_add_test_case kernel__no_backup
+ atf_add_test_case kernel__missing_set
+ atf_add_test_case kernel__too_many_args
+
+ atf_add_test_case modules__skip
+ atf_add_test_case modules__install
+ atf_add_test_case modules__too_many_args
+
+ atf_add_test_case sets__from_config
+ atf_add_test_case sets__from_args
+
+ atf_add_test_case etcupdate__skip__none
+ atf_add_test_case etcupdate__skip__no_etc
+ atf_add_test_case etcupdate__skip__destdir
+ atf_add_test_case etcupdate__some_etcs
+ atf_add_test_case etcupdate__missing_etc
+ atf_add_test_case etcupdate__too_many_args
+
+ atf_add_test_case postinstall__skip__none
+ atf_add_test_case postinstall__skip__no_etc
+ atf_add_test_case postinstall__some_etcs
+ atf_add_test_case postinstall__destdir
+ atf_add_test_case postinstall__autofix
+ atf_add_test_case postinstall__missing_etc
+ atf_add_test_case postinstall__explicit_args
+
+ atf_add_test_case clean__nothing
+ atf_add_test_case clean__only_tgzs
+ atf_add_test_case clean__too_many_args
+
+ atf_add_test_case auto__simple
+ atf_add_test_case auto__custom_releasedir
+ atf_add_test_case auto__skip_etcupdate
+ atf_add_test_case auto__skip_clean
+ atf_add_test_case auto__too_many_args
+
+ atf_add_test_case no_command
+ atf_add_test_case unknown_command
+ atf_add_test_case unknown_flag
+}
diff --git a/sysutils/sysupgrade/files/utils.subr b/sysutils/sysupgrade/files/utils.subr
new file mode 100644
index 00000000000..2d63bb78d1f
--- /dev/null
+++ b/sysutils/sysupgrade/files/utils.subr
@@ -0,0 +1,125 @@
+# Copyright 2012 Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * 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.
+# * Neither the name of Google Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
+# OWNER 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.
+
+# \file utils.subr
+# Miscellaneous utility functions and global variables.
+
+
+# Base name of the running script.
+Utils_ProgName="${0##*/}"
+
+
+# Prints a runtime error and exits.
+#
+# \param ... The message to print. Can be provided as multiple words and, in
+# that case, they are joined together by a single whitespace.
+utils_error() {
+ echo "${Utils_ProgName}: E: $*" 1>&2
+ exit 1
+}
+
+
+# Prints an informational message.
+#
+# \param ... The message to print. Can be provided as multiple words and, in
+# that case, they are joined together by a single whitespace.
+utils_info() {
+ echo "${Utils_ProgName}: I: $*" 1>&2
+}
+
+
+# Prints a runtime warning.
+#
+# \param ... The message to print. Can be provided as multiple words and, in
+# that case, they are joined together by a single whitespace.
+utils_warning() {
+ echo "${Utils_ProgName}: W: $*" 1>&2
+}
+
+
+# Prints an usage error and exits.
+#
+# \param ... The message to print. Can be provided as multiple words and, in
+# that case, they are joined together by a single whitespace.
+utils_usage_error() {
+ echo "${Utils_ProgName}: E: $*" 1>&2
+ echo "Type 'man ${Utils_ProgName}' for help" 1>&2
+ exit 1
+}
+
+
+# Executes a command with logging.
+#
+# \param ... The command to execute.
+#
+# \return The exit code of the executed command.
+utils_run() {
+ utils_info "Running '${@}' in $(pwd)"
+ local ret=0
+ "${@}" || ret="${?}"
+ if [ ${ret} -eq 0 ]; then
+ utils_info "Command finished successfully"
+ else
+ utils_warning "Command failed with code ${ret}"
+ fi
+ return "${ret}"
+}
+
+
+# Looks for an item in a list of items
+#
+# \param item The item to look for.
+# \param ... Contents of the list in which to search for the item.
+#
+# \return 0 if the item is found, 1 otherwise.
+utils_contains() {
+ local item="${1}"; shift
+
+ while [ ${#} -gt 0 ]; do
+ [ "${1}" != "${item}" ] || return 0
+ shift
+ done
+ return 1
+}
+
+
+# Filters a list based on a pattern.
+#
+# \param pattern Pattern to use for filtering.
+# \param ... Contents of the list to filter.
+#
+# \post The matched elements are printed on stdout.
+utils_filter() {
+ local pattern="${1}"; shift
+
+ for arg in "${@}"; do
+ case "${arg}" in
+ ${pattern}) echo "${arg}" ;;
+ esac
+ done
+}
diff --git a/sysutils/sysupgrade/files/utils_test.sh b/sysutils/sysupgrade/files/utils_test.sh
new file mode 100644
index 00000000000..48c7570236d
--- /dev/null
+++ b/sysutils/sysupgrade/files/utils_test.sh
@@ -0,0 +1,186 @@
+# Copyright 2012 Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * 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.
+# * Neither the name of Google Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
+# OWNER 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.
+
+
+atf_test_case global_progname
+global_progname_body() {
+ [ "${Utils_ProgName}" = utils_test ] \
+ || atf_fail "Invalid value in Utils_ProgName"
+}
+
+
+atf_test_case error
+error_body() {
+ if ( utils_error "This is" "a message"; echo "not seen" ) >out 2>err; then
+ atf_fail "utils_error did not exit with an error"
+ else
+ grep "utils_test: E: This is a message" err >/dev/null \
+ || atf_fail "Expected error message not found"
+ [ ! -s out ] || atf_fail "Unexpected output in stdout"
+ fi
+}
+
+
+atf_test_case info
+info_body() {
+ ( utils_info "This is" "a message"; echo "continuing" ) >out 2>err
+ grep "utils_test: I: This is a message" err >/dev/null \
+ || atf_fail "Expected info message not found"
+ grep "continuing" out >/dev/null || atf_fail "Execution aborted"
+}
+
+
+atf_test_case usage_error
+usage_error_body() {
+ if ( utils_usage_error "This is" "a message"; echo "not seen" ) >out 2>err
+ then
+ atf_fail "utils_usage_error did not exit with an error"
+ else
+ grep "utils_test: E: This is a message" err >/dev/null \
+ || atf_fail "Expected error message not found"
+ grep "Type 'man utils_test' for help" err >/dev/null \
+ || atf_fail "Expected instructional message not found"
+ [ ! -s out ] || atf_fail "Unexpected output in stdout"
+ fi
+}
+
+
+atf_test_case warning
+warning_body() {
+ ( utils_warning "This is" "a message"; echo "continuing" ) >out 2>err
+ grep "utils_test: W: This is a message" err >/dev/null \
+ || atf_fail "Expected info message not found"
+ grep "continuing" out >/dev/null || atf_fail "Execution aborted"
+}
+
+
+atf_test_case run__ok
+run__ok_body() {
+ cat >helper.sh <<EOF
+#! /bin/sh
+echo "This exits cleanly:" "\${@}"
+exit 0
+EOF
+ chmod +x helper.sh
+
+ utils_run ./helper.sh one two three >out 2>err \
+ || atf_fail "Got an unexpected error code"
+
+ cat >expout <<EOF
+This exits cleanly: one two three
+EOF
+ atf_check -o file:expout cat out
+
+ cat >experr <<EOF
+utils_test: I: Running './helper.sh one two three' in $(pwd)
+utils_test: I: Command finished successfully
+EOF
+ atf_check -o file:experr cat err
+}
+
+
+atf_test_case run__fail
+run__fail_body() {
+ cat >helper.sh <<EOF
+#! /bin/sh
+echo "This exits with an error:" "\${@}"
+exit 42
+EOF
+ chmod +x helper.sh
+
+ code=0
+ utils_run ./helper.sh one two three >out 2>err || code="${?}"
+ [ ${code} -eq 42 ] \
+ || atf_fail "Did not get the expected error code; got ${code}"
+
+ cat >expout <<EOF
+This exits with an error: one two three
+EOF
+ atf_check -o file:expout cat out
+
+ cat >experr <<EOF
+utils_test: I: Running './helper.sh one two three' in $(pwd)
+utils_test: W: Command failed with code 42
+EOF
+ atf_check -o file:experr cat err
+}
+
+
+atf_test_case contains__yes
+contains__yes_body() {
+ items="bar foo baz"
+ utils_contains foo ${items} || atf_fail "Element not found in list"
+}
+
+
+atf_test_case contains__no
+contains__no_body() {
+ items="bar foo baz"
+ ! utils_contains fo ${items} || atf_fail "Element found in list"
+}
+
+
+atf_test_case filter__no_items
+filter__no_items_body() {
+ atf_check_equal "" "$(utils_filter '*')"
+}
+
+
+atf_test_case filter__no_results
+filter__no_results_body() {
+ items="abc a1 foo a2 a3 bar"
+ atf_check_equal "" "$(utils_filter '*a' ${items})"
+}
+
+
+atf_test_case filter__some_results
+filter__some_results_body() {
+ items="abc a1 foo a2 a3 bar"
+ atf_check_equal "a1 a2 a3" "$(utils_filter 'a[0-9]*' ${items})"
+}
+
+
+atf_init_test_cases() {
+ atf_add_test_case global_progname
+
+ atf_add_test_case error
+ atf_add_test_case info
+ atf_add_test_case warning
+
+ atf_add_test_case usage_error
+
+ atf_add_test_case run__ok
+ atf_add_test_case run__fail
+
+ atf_add_test_case contains__yes
+ atf_add_test_case contains__no
+
+ atf_add_test_case filter__no_items
+ atf_add_test_case filter__no_results
+ atf_add_test_case filter__some_results
+}