diff --git a/configure.ac b/configure.ac
index 6e3b9d8d53..3acbca6446 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1015,6 +1015,9 @@ AC_CONFIG_FILES([Makefile
src/bin/cfgmgr/plugins/Makefile
src/bin/cfgmgr/plugins/tests/Makefile
src/bin/cfgmgr/tests/Makefile
+ src/bin/dbutil/Makefile
+ src/bin/dbutil/tests/Makefile
+ src/bin/dbutil/tests/testdata/Makefile
src/bin/host/Makefile
src/bin/loadzone/Makefile
src/bin/loadzone/tests/correct/Makefile
@@ -1028,8 +1031,8 @@ AC_CONFIG_FILES([Makefile
src/bin/ddns/tests/Makefile
src/bin/dhcp6/Makefile
src/bin/dhcp6/tests/Makefile
- src/bin/dhcp4/Makefile
- src/bin/dhcp4/tests/Makefile
+ src/bin/dhcp4/Makefile
+ src/bin/dhcp4/tests/Makefile
src/bin/resolver/Makefile
src/bin/resolver/tests/Makefile
src/bin/sockcreator/Makefile
@@ -1143,6 +1146,9 @@ AC_OUTPUT([doc/version.ent
src/bin/cmdctl/run_b10-cmdctl.sh
src/bin/cmdctl/tests/cmdctl_test
src/bin/cmdctl/cmdctl.spec.pre
+ src/bin/dbutil/dbutil.py
+ src/bin/dbutil/run_dbutil.sh
+ src/bin/dbutil/tests/dbutil_test.sh
src/bin/ddns/ddns.py
src/bin/xfrin/tests/xfrin_test
src/bin/xfrin/xfrin.py
@@ -1226,6 +1232,8 @@ AC_OUTPUT([doc/version.ent
chmod +x src/bin/zonemgr/run_b10-zonemgr.sh
chmod +x src/bin/bind10/run_bind10.sh
chmod +x src/bin/cmdctl/tests/cmdctl_test
+ chmod +x src/bin/dbutil/run_dbutil.sh
+ chmod +x src/bin/dbutil/tests/dbutil_test.sh
chmod +x src/bin/xfrin/tests/xfrin_test
chmod +x src/bin/xfrout/tests/xfrout_test
chmod +x src/bin/zonemgr/tests/zonemgr_test
diff --git a/src/bin/Makefile.am b/src/bin/Makefile.am
index 7c6cdb8bde..499e2097ec 100644
--- a/src/bin/Makefile.am
+++ b/src/bin/Makefile.am
@@ -1,4 +1,4 @@
SUBDIRS = bind10 bindctl cfgmgr ddns loadzone msgq host cmdctl auth xfrin \
- xfrout usermgr zonemgr stats tests resolver sockcreator dhcp4 dhcp6
+ xfrout usermgr zonemgr stats tests resolver sockcreator dhcp4 dhcp6 dbutil
check-recursive: all-recursive
diff --git a/src/bin/dbutil/Makefile.am b/src/bin/dbutil/Makefile.am
new file mode 100644
index 0000000000..f328cb0fef
--- /dev/null
+++ b/src/bin/dbutil/Makefile.am
@@ -0,0 +1,38 @@
+SUBDIRS = . tests
+
+bin_SCRIPTS = b10-dbutil
+man_MANS = b10-dbutil.8
+
+nodist_pylogmessage_PYTHON = $(PYTHON_LOGMSGPKG_DIR)/work/dbutil_messages.py
+pylogmessagedir = $(pyexecdir)/isc/log_messages/
+
+EXTRA_DIST = $(man_MANS) b10-dbutil.xml dbutil_messages.mes
+
+noinst_SCRIPTS = run_dbutil.sh
+
+CLEANFILES = b10-dbutil b10-dbutil.pyc
+CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/dbutil_messages.py
+CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/dbutil_messages.pyc
+
+if ENABLE_MAN
+
+b10-dbutil.8: b10-dbutil.xml
+ xsltproc --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-dbutil.xml
+
+endif
+
+# Define rule to build logging source files from message file
+$(PYTHON_LOGMSGPKG_DIR)/work/dbutil_messages.py : dbutil_messages.mes
+ $(top_builddir)/src/lib/log/compiler/message \
+ -d $(PYTHON_LOGMSGPKG_DIR)/work -p $(srcdir)/dbutil_messages.mes
+
+b10-dbutil: dbutil.py $(PYTHON_LOGMSGPKG_DIR)/work/dbutil_messages.py
+ $(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" \
+ -e "s|@@SYSCONFDIR@@|@sysconfdir@|" \
+ -e "s|@@LIBEXECDIR@@|$(pkglibexecdir)|" dbutil.py >$@
+ chmod a+x $@
+
+CLEANDIRS = __pycache__
+
+clean-local:
+ rm -rf $(CLEANDIRS)
diff --git a/src/bin/dbutil/b10-dbutil.8 b/src/bin/dbutil/b10-dbutil.8
new file mode 100644
index 0000000000..437a69d1ec
--- /dev/null
+++ b/src/bin/dbutil/b10-dbutil.8
@@ -0,0 +1,92 @@
+'\" t
+.\" Title: b10-dbutil
+.\" Author: [FIXME: author] [see http://docbook.sf.net/el/author]
+.\" Generator: DocBook XSL Stylesheets v1.75.2
+.\" Date: March 20, 2012
+.\" Manual: BIND10
+.\" Source: BIND10
+.\" Language: English
+.\"
+.TH "B10\-DBUTIL" "8" "March 20, 2012" "BIND10" "BIND10"
+.\" -----------------------------------------------------------------
+.\" * Define some portability stuff
+.\" -----------------------------------------------------------------
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.\" http://bugs.debian.org/507673
+.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.\" -----------------------------------------------------------------
+.\" * set default formatting
+.\" -----------------------------------------------------------------
+.\" disable hyphenation
+.nh
+.\" disable justification (adjust text to left margin only)
+.ad l
+.\" -----------------------------------------------------------------
+.\" * MAIN CONTENT STARTS HERE *
+.\" -----------------------------------------------------------------
+.SH "NAME"
+b10-dbutil \- Zone Database Maintenance Utility
+.SH "SYNOPSIS"
+.HP \w'\fBb10\-dbutil\ \-\-check\fR\ 'u
+\fBb10\-dbutil \-\-check\fR [\-\-verbose] [\-\-quiet] [\fIdbfile\fR]
+.HP \w'\fBb10\-dbutil\ \-\-upgrade\fR\ 'u
+\fBb10\-dbutil \-\-upgrade\fR [\-\-noconfirm] [\-\-verbose] [\-\-quiet] [\fIdbfile\fR]
+.SH "DESCRIPTION"
+.PP
+The
+\fBb10\-dbutil\fR
+utility is a general administration utility for SQL databases\&. (Currently only SQLite is supported by BIND 10\&.) It can report the current verion of the schema, and upgrade an existing database to the latest version of the schema\&.
+.PP
+
+\fBb10\-dbutil\fR
+operates in one of two modes, check mode or upgrade mode\&.
+.PP
+In check mode (\fBb10\-dbutil \-\-check\fR), the utility reads the version of the database schema from the database and prints it\&. It will tell you whether the schema is at the latest version supported by BIND 10\&. Exit status is 0 if the schema is at the correct version, 1 if the schema is at an older version, 2 if the schema is at a version not yet supported by this version of b10\-dbutil\&. Any higher value indicates an error during command\-line parsing or execution\&.
+.PP
+When the upgrade function is selected (\fBb10\-dbutil \-\-upgrade\fR), the utility takes a copy of the database, then upgrades it to the latest version of the schema\&. The contents of the database remain intact\&. (The backup file is a file in the same directory as the database file\&. It has the same name, with "\&.backup" appended to it\&. If a file of that name already exists, the file will have the suffix "\&.backup\-1"\&. If that exists, the file will be suffixed "\&.backup\-2", and so on)\&. Exit status is 0 if the upgrade is either succesful or aborted by the user, and non\-zero if there is an error\&.
+.PP
+When upgrading the database, it is
+\fIstrongly\fR
+recommended that BIND 10 not be running while the upgrade is in progress\&.
+.SH "ARGUMENTS"
+.PP
+The arguments are as follows:
+.PP
+\fB\-\-check\fR
+.RS 4
+Selects the version check function, which reports the current version of the database\&. This is incompatible with the \-\-upgrade option\&.
+.RE
+.PP
+\fB\-\-noconfirm\fR
+.RS 4
+Only valid with \-\-upgrade, this disables the prompt\&. Normally the utility will print a warning that an upgrade is about to take place and request that you type "Yes" to continue\&. If this switch is given on the command line, no prompt will be issued: the utility will just perform the upgrade\&.
+.RE
+.PP
+\fB\-\-upgrade\fR
+.RS 4
+Selects the upgrade function, which upgrades the database to the latest version of the schema\&. This is incompatible with the \-\-upgrade option\&.
+.sp
+The upgrade function will upgrade a BIND 10 database \- no matter how old the schema \- preserving all data\&. A backup file is created before the upgrade (with the same name as the database, but with "\&.backup" suffixed to it)\&. If the upgrade fails, this file can be copied back to restore the original database\&.
+.RE
+.PP
+\fB\-\-verbose\fR
+.RS 4
+Enable verbose mode\&. Each SQL command issued by the utility will be printed to stderr before it is executed\&.
+.RE
+.PP
+\fB\-\-quiet\fR
+.RS 4
+Enable quiet mode\&. No output is printed, except errors during command\-line argument parsing, or the user confirmation dialog\&.
+.RE
+.PP
+\fB\fIdbfile\fR\fR
+.RS 4
+Name of the database file to check of upgrade\&.
+.RE
+.SH "COPYRIGHT"
+.br
+Copyright \(co 2012 Internet Systems Consortium, Inc. ("ISC")
+.br
diff --git a/src/bin/dbutil/b10-dbutil.xml b/src/bin/dbutil/b10-dbutil.xml
new file mode 100644
index 0000000000..c1c0dee1ca
--- /dev/null
+++ b/src/bin/dbutil/b10-dbutil.xml
@@ -0,0 +1,192 @@
+]>
+
+
+
+
+
+ March 20, 2012
+
+
+
+ b10-dbutil
+ 8
+ BIND10
+
+
+
+ b10-dbutil
+ Zone Database Maintenance Utility
+
+
+
+
+ 2012
+ Internet Systems Consortium, Inc. ("ISC")
+
+
+
+
+
+ b10-dbutil --check
+ --verbose
+ --quiet
+ dbfile
+
+
+ b10-dbutil --upgrade
+ --noconfirm
+ --verbose
+ --quiet
+ dbfile
+
+
+
+
+ DESCRIPTION
+
+ The b10-dbutil utility is a general administration
+ utility for SQL databases. (Currently only SQLite is supported by
+ BIND 10.) It can report the current verion of the schema, and upgrade
+ an existing database to the latest version of the schema.
+
+
+
+ b10-dbutil operates in one of two modes, check mode
+ or upgrade mode.
+
+
+
+ In check mode (b10-dbutil --check), the
+ utility reads the version of the database schema from the database
+ and prints it. It will tell you whether the schema is at the latest
+ version supported by BIND 10. Exit status is 0 if the schema is at
+ the correct version, 1 if the schema is at an older version, 2 if
+ the schema is at a version not yet supported by this version of
+ b10-dbutil. Any higher value indicates an error during command-line
+ parsing or execution.
+
+
+
+ When the upgrade function is selected
+ (b10-dbutil --upgrade), the
+ utility takes a copy of the database, then upgrades it to the latest
+ version of the schema. The contents of the database remain intact.
+ (The backup file is a file in the same directory as the database
+ file. It has the same name, with ".backup" appended to it. If a
+ file of that name already exists, the file will have the suffix
+ ".backup-1". If that exists, the file will be suffixed ".backup-2",
+ and so on). Exit status is 0 if the upgrade is either succesful or
+ aborted by the user, and non-zero if there is an error.
+
+
+
+ When upgrading the database, it is strongly
+ recommended that BIND 10 not be running while the upgrade is in
+ progress.
+
+
+
+
+
+ ARGUMENTS
+
+ The arguments are as follows:
+
+
+
+
+
+
+
+ Selects the version check function, which reports the
+ current version of the database. This is incompatible
+ with the --upgrade option.
+
+
+
+
+
+
+
+
+
+ Only valid with --upgrade, this disables the prompt.
+ Normally the utility will print a warning that an upgrade is
+ about to take place and request that you type "Yes" to continue.
+ If this switch is given on the command line, no prompt will
+ be issued: the utility will just perform the upgrade.
+
+
+
+
+
+
+
+
+
+ Selects the upgrade function, which upgrades the database
+ to the latest version of the schema. This is incompatible
+ with the --upgrade option.
+
+
+ The upgrade function will upgrade a BIND 10 database - no matter how
+ old the schema - preserving all data. A backup file is created
+ before the upgrade (with the same name as the database, but with
+ ".backup" suffixed to it). If the upgrade fails, this file can
+ be copied back to restore the original database.
+
+
+
+
+
+
+
+
+
+ Enable verbose mode. Each SQL command issued by the
+ utility will be printed to stderr before it is executed.
+
+
+
+
+
+
+
+
+ Enable quiet mode. No output is printed, except errors during
+ command-line argument parsing, or the user confirmation dialog.
+
+
+
+
+
+
+
+
+
+
+ Name of the database file to check of upgrade.
+
+
+
+
+
+
+
+
diff --git a/src/bin/dbutil/dbutil.py.in b/src/bin/dbutil/dbutil.py.in
new file mode 100755
index 0000000000..81f351e7ad
--- /dev/null
+++ b/src/bin/dbutil/dbutil.py.in
@@ -0,0 +1,608 @@
+#!@PYTHON@
+
+# Copyright (C) 2012 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""
+@file Dabase Utilities
+
+This file holds the "dbutil" program, a general utility program for doing
+management of the BIND 10 database. There are two modes of operation:
+
+ b10-dbutil --check [--verbose] database
+ b10-dbutil --upgrade [--noconfirm] [--verbose] database
+
+The first form checks the version of the given database. The second form
+upgrades the database to the latest version of the schema, omitting the
+warning prompt if --noconfirm is given.
+
+For maximum safety, prior to the upgrade a backup database is created.
+The is the database name with ".backup" appended to it (or ".backup-n" if
+".backup" already exists). This is used to restore the database if the
+upgrade fails.
+"""
+
+# Exit codes
+# These are defined here because one of them is already used before most
+# of the import statements.
+EXIT_SUCCESS = 0
+EXIT_NEED_UPDATE = 1
+EXIT_VERSION_TOO_HIGH = 2
+EXIT_COMMAND_ERROR = 3
+EXIT_READ_ERROR = 4
+EXIT_UPGRADE_ERROR = 5
+EXIT_UNCAUGHT_EXCEPTION = 6
+
+import sys; sys.path.append("@@PYTHONPATH@@")
+
+# Normally, python exits with a status code of 1 on uncaught exceptions
+# Since we reserve exit status 1 for 'database needs upgrade', we
+# override the excepthook to exit with a different status
+def my_except_hook(a, b, c):
+ sys.__excepthook__(a,b,c)
+ sys.exit(EXIT_UNCAUGHT_EXCEPTION)
+sys.excepthook = my_except_hook
+
+import os, sqlite3, shutil
+from optparse import OptionParser
+import isc.util.process
+import isc.log
+from isc.log_messages.dbutil_messages import *
+
+isc.log.init("b10-dbutil")
+logger = isc.log.Logger("dbutil")
+isc.util.process.rename()
+
+TRACE_BASIC = logger.DBGLVL_TRACE_BASIC
+
+
+# @brief Version String
+# This is the version displayed to the user. It comprises the module name,
+# the module version number, and the overall BIND 10 version number (set in
+# configure.ac)
+VERSION = "b10-dbutil 20120319 (BIND 10 @PACKAGE_VERSION@)"
+
+# @brief Statements to Update the Database
+# These are in the form of a list of dictionaries, each of which contains the
+# information to perform an incremental upgrade from one version of the
+# database to the next. The information is:
+#
+# a) from: (major, minor) version that the database is expected to be at
+# to perform this upgrade.
+# b) to: (major, minor) version of the database to which this set of statements
+# upgrades the database to. (This is used for documentation purposes,
+# and to update the schema_version table when the upgrade is complete.)
+# c) statements: List of SQL statments to perform the upgrade.
+#
+# The incremental upgrades are performed one after the other. If the version
+# of the database does not exactly match that required for the incremental
+# upgrade, the upgrade is skipped. For this reason, the list must be in
+# ascending order (e.g. upgrade 1.0 to 2.0, 2.0 to 2.1, 2.1 to 2.2 etc.).
+#
+# Note that apart from the 1.0 to 2.0 upgrade, no upgrade need alter the
+# schema_version table: that is done by the upgrade process using the
+# information in the "to" field.
+UPGRADES = [
+ {'from': (1, 0), 'to': (2, 0),
+ 'statements': [
+
+ # Move to the latest "V1" state of the database if not there
+ # already.
+ "CREATE TABLE IF NOT EXISTS diffs (" +
+ "id INTEGER PRIMARY KEY, " +
+ "zone_id INTEGER NOT NULL," +
+ "version INTEGER NOT NULL, " +
+ "operation INTEGER NOT NULL, " +
+ "name STRING NOT NULL COLLATE NOCASE, " +
+ "rrtype STRING NOT NULL COLLATE NOCASE, " +
+ "ttl INTEGER NOT NULL, " +
+ "rdata STRING NOT NULL)",
+
+ # Within SQLite with can only rename tables and add columns; we
+ # can't drop columns nor can we alter column characteristics.
+ # So the strategy is to rename the table, create the new table,
+ # then copy all data across. This means creating new indexes
+ # as well; these are created after the data has been copied.
+
+ # zones table
+ "DROP INDEX zones_byname",
+ "ALTER TABLE zones RENAME TO old_zones",
+ "CREATE TABLE zones (" +
+ "id INTEGER PRIMARY KEY, " +
+ "name TEXT NOT NULL COLLATE NOCASE, " +
+ "rdclass TEXT NOT NULL COLLATE NOCASE DEFAULT 'IN', " +
+ "dnssec BOOLEAN NOT NULL DEFAULT 0)",
+ "INSERT INTO ZONES " +
+ "SELECT id, name, rdclass, dnssec FROM old_zones",
+ "CREATE INDEX zones_byname ON zones (name)",
+ "DROP TABLE old_zones",
+
+ # records table
+ "DROP INDEX records_byname",
+ "DROP INDEX records_byrname",
+ "ALTER TABLE records RENAME TO old_records",
+ "CREATE TABLE records (" +
+ "id INTEGER PRIMARY KEY, " +
+ "zone_id INTEGER NOT NULL, " +
+ "name TEXT NOT NULL COLLATE NOCASE, " +
+ "rname TEXT NOT NULL COLLATE NOCASE, " +
+ "ttl INTEGER NOT NULL, " +
+ "rdtype TEXT NOT NULL COLLATE NOCASE, " +
+ "sigtype TEXT COLLATE NOCASE, " +
+ "rdata TEXT NOT NULL)",
+ "INSERT INTO records " +
+ "SELECT id, zone_id, name, rname, ttl, rdtype, sigtype, " +
+ "rdata FROM old_records",
+ "CREATE INDEX records_byname ON records (name)",
+ "CREATE INDEX records_byrname ON records (rname)",
+ "CREATE INDEX records_bytype_and_rname ON records (rdtype, rname)",
+ "DROP TABLE old_records",
+
+ # nsec3 table
+ "DROP INDEX nsec3_byhash",
+ "ALTER TABLE nsec3 RENAME TO old_nsec3",
+ "CREATE TABLE nsec3 (" +
+ "id INTEGER PRIMARY KEY, " +
+ "zone_id INTEGER NOT NULL, " +
+ "hash TEXT NOT NULL COLLATE NOCASE, " +
+ "owner TEXT NOT NULL COLLATE NOCASE, " +
+ "ttl INTEGER NOT NULL, " +
+ "rdtype TEXT NOT NULL COLLATE NOCASE, " +
+ "rdata TEXT NOT NULL)",
+ "INSERT INTO nsec3 " +
+ "SELECT id, zone_id, hash, owner, ttl, rdtype, rdata " +
+ "FROM old_nsec3",
+ "CREATE INDEX nsec3_byhash ON nsec3 (hash)",
+ "DROP TABLE old_nsec3",
+
+ # diffs table
+ "ALTER TABLE diffs RENAME TO old_diffs",
+ "CREATE TABLE diffs (" +
+ "id INTEGER PRIMARY KEY, " +
+ "zone_id INTEGER NOT NULL, " +
+ "version INTEGER NOT NULL, " +
+ "operation INTEGER NOT NULL, " +
+ "name TEXT NOT NULL COLLATE NOCASE, " +
+ "rrtype TEXT NOT NULL COLLATE NOCASE, " +
+ "ttl INTEGER NOT NULL, " +
+ "rdata TEXT NOT NULL)",
+ "INSERT INTO diffs " +
+ "SELECT id, zone_id, version, operation, name, rrtype, " +
+ "ttl, rdata FROM old_diffs",
+ "DROP TABLE old_diffs",
+
+ # Schema table. This is updated to include a second column for
+ # future changes. The idea is that if a version of BIND 10 is
+ # written for schema M.N, it should be able to work for all
+ # versions of N; if not, M must be incremented.
+ #
+ # For backwards compatibility, the column holding the major
+ # version number is left named "version".
+ "ALTER TABLE schema_version " +
+ "ADD COLUMN minor INTEGER NOT NULL DEFAULT 0"
+ ]
+ }
+
+# To extend this, leave the above statements in place and add another
+# dictionary to the list. The "from" version should be (2, 0), the "to"
+# version whatever the version the update is to, and the SQL statements are
+# the statements required to perform the upgrade. This way, the upgrade
+# program will be able to upgrade both a V1.0 and a V2.0 database.
+]
+
+class DbutilException(Exception):
+ """
+ @brief Exception class to indicate error exit
+ """
+ pass
+
+class Database:
+ """
+ @brief Database Encapsulation
+
+ Encapsulates the SQL database, both the connection and the cursor. The
+ methods will cause a program exit on any error.
+ """
+ def __init__(self, db_file):
+ """
+ @brief Constructor
+
+ @param db_file Name of the database file
+ """
+ self.connection = None
+ self.cursor = None
+ self.db_file = db_file
+ self.backup_file = None
+
+ def open(self):
+ """
+ @brief Open Database
+
+ Opens the passed file as an sqlite3 database and stores a connection
+ and a cursor.
+ """
+ if not os.path.exists(self.db_file):
+ raise DbutilException("database " + self.db_file +
+ " does not exist");
+
+ try:
+ self.connection = sqlite3.connect(self.db_file)
+ self.connection.isolation_level = None # set autocommit
+ self.cursor = self.connection.cursor()
+ except sqlite3.OperationalError as ex:
+ raise DbutilException("unable to open " + self.db_file +
+ " - " + str(ex))
+
+ def close(self):
+ """
+ @brief Closes the database
+ """
+ if self.connection is not None:
+ self.connection.close()
+
+ def execute(self, statement):
+ """
+ @brief Execute Statement
+
+ Executes the given statement, exiting the program on error.
+
+ @param statement SQL statement to execute
+ """
+ logger.debug(TRACE_BASIC, DBUTIL_EXECUTE, statement)
+
+ try:
+ self.cursor.execute(statement)
+ except Exception as ex:
+ logger.error(DBUTIL_STATEMENT_ERROR, statement, ex)
+ raise DbutilException(str(ex))
+
+ def result(self):
+ """
+ @brief Return result of last execute
+
+ Returns a single row that is the result of the last "execute".
+ """
+ return self.cursor.fetchone()
+
+ def backup(self):
+ """
+ @brief Backup Database
+
+ Attempts to copy the given database file to a backup database, the
+ backup database file being the file name with ".backup" appended.
+ If the ".backup" file exists, a new name is constructed by appending
+ ".backup-n" (n starting at 1) and the action repeated until an
+ unused filename is found.
+
+ @param db_file Database file to backup
+ """
+ if not os.path.exists(self.db_file):
+ raise DbutilException("database " + self.db_file +
+ " does not exist");
+
+ self.backup_file = self.db_file + ".backup"
+ count = 0
+ while os.path.exists(self.backup_file):
+ count = count + 1
+ self.backup_file = self.db_file + ".backup-" + str(count)
+
+ # Do the backup
+ shutil.copyfile(self.db_file, self.backup_file)
+ logger.info(DBUTIL_BACKUP, self.db_file, self.backup_file)
+
+def prompt_user():
+ """
+ @brief Prompt the User
+
+ Explains about the upgrade and requests authorisation to continue.
+
+ @return True if user entered 'Yes', False if 'No'
+ """
+ sys.stdout.write(
+"""You have selected the upgrade option. This will upgrade the schema of the
+selected BIND 10 zone database to the latest version.
+
+The utility will take a copy of the zone database file before executing so, in
+the event of a problem, you will be able to restore the zone database from
+the backup. To ensure that the integrity of this backup, please ensure that
+BIND 10 is not running before continuing.
+""")
+ yes_entered = False
+ no_entered = False
+ while (not yes_entered) and (not no_entered):
+ sys.stdout.write("Enter 'Yes' to proceed with the upgrade, " +
+ "'No' to exit the program: \n")
+ response = sys.stdin.readline()
+ if response.lower() == "yes\n":
+ yes_entered = True
+ elif response.lower() == "no\n":
+ no_entered = True
+ else:
+ sys.stdout.write("Please enter 'Yes' or 'No'\n")
+
+ return yes_entered
+
+
+def version_string(version):
+ """
+ @brief Format Database Version
+
+ Converts a (major, minor) tuple into a 'Vn.m' string.
+
+ @param version Version tuple to convert
+
+ @return Version string
+ """
+ return "V" + str(version[0]) + "." + str(version[1])
+
+
+def compare_versions(first, second):
+ """
+ @brief Compare Versions
+
+ Compares two database version numbers.
+
+ @param first First version number to check (in the form of a
+ "(major, minor)" tuple).
+ @param second Second version number to check (in the form of a
+ "(major, minor)" tuple).
+
+ @return -1, 0, +1 if "first" is <, ==, > "second"
+ """
+ if first == second:
+ return 0
+
+ elif ((first[0] < second[0]) or
+ ((first[0] == second[0]) and (first[1] < second[1]))):
+ return -1
+
+ else:
+ return 1
+
+
+def get_latest_version():
+ """
+ @brief Returns the version to which this utility can upgrade the database
+
+ This is the 'to' version held in the last element of the upgrades list
+ """
+ return UPGRADES[-1]['to']
+
+
+def get_version(db):
+ """
+ @brief Return version of database
+
+ @return Version of database in form (major version, minor version)
+ """
+
+ # Get the version information.
+ db.execute("SELECT * FROM schema_version")
+ result = db.result()
+ if result is None:
+ raise DbutilException("nothing in schema_version table")
+
+ major = result[0]
+ if (major == 1):
+ # If the version number is 1, there will be no "minor" column, so
+ # assume a minor version number of 0.
+ minor = 0
+ else:
+ minor = result[1]
+
+ result = db.result()
+ if result is not None:
+ raise DbutilException("too many rows in schema_version table")
+
+ return (major, minor)
+
+
+def check_version(db):
+ """
+ @brief Check the version
+
+ Checks the version of the database and the latest version, and advises if
+ an upgrade is needed.
+
+ @param db Database object
+
+ returns 0 if the database is up to date
+ returns EXIT_NEED_UPDATE if the database needs updating
+ returns EXIT_VERSION_TOO_HIGH if the database is at a later version
+ than this program knows about
+ These return values are intended to be passed on to sys.exit.
+ """
+ current = get_version(db)
+ latest = get_latest_version()
+
+ match = compare_versions(current, latest)
+ if match == 0:
+ logger.info(DBUTIL_VERSION_CURRENT, version_string(current))
+ logger.info(DBUTIL_CHECK_OK)
+ return EXIT_SUCCESS
+ elif match < 0:
+ logger.info(DBUTIL_VERSION_LOW, version_string(current),
+ version_string(latest))
+ logger.info(DBUTIL_CHECK_UPGRADE_NEEDED)
+ return EXIT_NEED_UPDATE
+ else:
+ logger.warn(DBUTIL_VERSION_HIGH, version_string(current),
+ version_string(get_latest_version()))
+ logger.info(DBUTIL_UPGRADE_DBUTIL)
+ return EXIT_VERSION_TOO_HIGH
+
+def perform_upgrade(db, upgrade):
+ """
+ @brief Perform upgrade
+
+ Performs the upgrade. At the end of the upgrade, updates the schema_version
+ table with the expected version.
+
+ @param db Database object
+ @param upgrade Upgrade dictionary, holding "from", "to" and "statements".
+ """
+ logger.info(DBUTIL_UPGRADING, version_string(upgrade['from']),
+ version_string(upgrade['to']))
+ for statement in upgrade['statements']:
+ db.execute(statement)
+
+ # Update the version information
+ db.execute("DELETE FROM schema_version")
+ db.execute("INSERT INTO schema_version VALUES (" +
+ str(upgrade['to'][0]) + "," + str(upgrade['to'][1]) + ")")
+
+
+def perform_all_upgrades(db):
+ """
+ @brief Performs all the upgrades
+
+ @brief db Database object
+
+ For each upgrade, checks that the database is at the expected version.
+ If so, calls perform_upgrade to update the database.
+ """
+ match = compare_versions(get_version(db), get_latest_version())
+ if match == 0:
+ logger.info(DBUTIL_UPGRADE_NOT_NEEDED)
+
+ elif match > 0:
+ logger.warn(DBUTIL_UPGRADE_NOT_POSSIBLE)
+
+ else:
+ # Work our way through all upgrade increments
+ count = 0
+ for upgrade in UPGRADES:
+ if compare_versions(get_version(db), upgrade['from']) == 0:
+ perform_upgrade(db, upgrade)
+ count = count + 1
+
+ if count > 0:
+ logger.info(DBUTIL_UPGRADE_SUCCESFUL)
+ else:
+ # Should not get here, as we established earlier that the database
+ # was not at the latest version so we should have upgraded.
+ raise DbutilException("internal error in upgrade tool - no " +
+ "upgrade was performed on an old version " +
+ "the database")
+
+
+def parse_command():
+ """
+ @brief Parse Command
+
+ Parses the command line and sets the global command options.
+
+ @return Tuple of parser options and parser arguments
+ """
+ usage = ("usage: %prog --check [options] db_file\n" +
+ " %prog --upgrade [--noconfirm] [options] db_file")
+ parser = OptionParser(usage = usage, version = VERSION)
+ parser.add_option("-c", "--check", action="store_true",
+ dest="check", default=False,
+ help="Print database version and check if it " +
+ "needs upgrading")
+ parser.add_option("-n", "--noconfirm", action="store_true",
+ dest="noconfirm", default=False,
+ help="Do not prompt for confirmation before upgrading")
+ parser.add_option("-u", "--upgrade", action="store_true",
+ dest="upgrade", default=False,
+ help="Upgrade the database file to the latest version")
+ parser.add_option("-v", "--verbose", action="store_true",
+ dest="verbose", default=False,
+ help="Print SQL statements as they are executed")
+ parser.add_option("-q", "--quiet", action="store_true",
+ dest="quiet", default=False,
+ help="Don't print any info, warnings or errors")
+ (options, args) = parser.parse_args()
+
+ # Set the database file on which to operate
+ if (len(args) > 1):
+ logger.error(DBUTIL_TOO_MANY_ARGUMENTS)
+ parser.print_usage()
+ sys.exit(EXIT_COMMAND_ERROR)
+ elif len(args) == 0:
+ logger.error(DBUTIL_NO_FILE)
+ parser.print_usage()
+ sys.exit(EXIT_COMMAND_ERROR)
+
+ # Check for conflicting options. If some are found, output a suitable
+ # error message and print the usage.
+ if options.check and options.upgrade:
+ logger.error(DBUTIL_COMMAND_UPGRADE_CHECK)
+ elif (not options.check) and (not options.upgrade):
+ logger.error(DBUTIL_COMMAND_NONE)
+ elif (options.check and options.noconfirm):
+ logger.error(DBUTIL_CHECK_NOCONFIRM)
+ else:
+ return (options, args)
+
+ # Only get here on conflicting options
+ parser.print_usage()
+ sys.exit(EXIT_COMMAND_ERROR)
+
+
+if __name__ == "__main__":
+ (options, args) = parse_command()
+
+ if options.verbose:
+ isc.log.init("b10-dbutil", "DEBUG", 99)
+ logger = isc.log.Logger("dbutil")
+ elif options.quiet:
+ # We don't use FATAL, so setting the logger to use
+ # it should essentially make it silent.
+ isc.log.init("b10-dbutil", "FATAL")
+ logger = isc.log.Logger("dbutil")
+
+ db = Database(args[0])
+ exit_code = EXIT_SUCCESS
+
+ logger.info(DBUTIL_FILE, args[0])
+ if options.check:
+ # Check database - open, report, and close
+ try:
+ db.open()
+ exit_code = check_version(db)
+ db.close()
+ except Exception as ex:
+ logger.error(DBUTIL_CHECK_ERROR, ex)
+ exit_code = EXIT_READ_ERROR
+
+ elif options.upgrade:
+ # Upgrade. Check if this is what they really want to do
+ if not options.noconfirm:
+ proceed = prompt_user()
+ if not proceed:
+ logger.info(DBUTIL_UPGRADE_CANCELED)
+ sys.exit(EXIT_SUCCESS)
+
+ # It is. Do a backup then do the upgrade.
+ in_progress = False
+ try:
+ db.backup()
+ db.open()
+ in_progress = True
+ perform_all_upgrades(db)
+ db.close()
+ except Exception as ex:
+ if in_progress:
+ logger.error(DBUTIL_UPGRADE_FAILED, ex)
+ logger.warn(DBUTIL_DATABASE_MAY_BE_CORRUPT, db.db_file,
+ db.backup_file)
+ else:
+ logger.error(DBUTIL_UPGRADE_PREPARATION_FAILED, ex)
+ logger.info(DBUTIL_UPGRADE_NOT_ATTEMPTED)
+ exit_code = EXIT_UPGRADE_ERROR
+
+ sys.exit(exit_code)
diff --git a/src/bin/dbutil/dbutil_messages.mes b/src/bin/dbutil/dbutil_messages.mes
new file mode 100644
index 0000000000..90ede92283
--- /dev/null
+++ b/src/bin/dbutil/dbutil_messages.mes
@@ -0,0 +1,114 @@
+# Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+# No namespace declaration - these constants go in the global namespace
+# of the ddns messages python module.
+
+# When you add a message to this file, it is a good idea to run
+# /tools/reorder_message_file.py to make sure the
+# messages are in the correct order.
+
+% DBUTIL_BACKUP created backup of %1 in %2
+A backup for the given database file was created. Same of original file and
+backup are given in the output message.
+
+% DBUTIL_CHECK_ERROR unable to check database version: %1
+There was an error while trying to check the current version of the database
+schema. The error is shown in the message.
+
+% DBUTIL_CHECK_NOCONFIRM --noconfirm is not compatible with --check
+b10-dbutil was called with --check and --noconfirm. --noconfirm only has
+meaning with --upgrade, so this is considered an error.
+
+% DBUTIL_CHECK_OK this is the latest version of the database schema. No upgrade is required
+The database schema version has been checked, and is up to date.
+No action is required.
+
+% DBUTIL_CHECK_UPGRADE_NEEDED re-run this program with the --upgrade switch to upgrade
+The database schema version is not up to date, and an update is required.
+Please run the dbutil tool again, with the --upgrade argument.
+
+% DBUTIL_COMMAND_NONE must select one of --check or --upgrade
+b10-dbutil was called with neither --check nor --upgrade. One action must be
+provided.
+
+% DBUTIL_COMMAND_UPGRADE_CHECK --upgrade is not compatible with --check
+b10-dbutil was called with both the commands --upgrade and --check. Only one
+action can be performed at a time.
+
+% DBUTIL_DATABASE_MAY_BE_CORRUPT database file %1 may be corrupt, restore it from backup (%2)
+The upgrade failed while it was in progress; the database may now be in an
+inconsistent state, and it is advised to restore it from the backup that was
+created when b10-dbutil started.
+
+% DBUTIL_EXECUTE Executing SQL statement: %1
+Debug message; the given SQL statement is executed
+
+% DBUTIL_FILE Database file: %1
+The database file that is being checked.
+
+% DBUTIL_NO_FILE must supply name of the database file to upgrade
+b10-dbutil was called without a database file. Currently, it cannot find this
+file on its own, and it must be provided.
+
+% DBUTIL_STATEMENT_ERROR failed to execute %1: %2
+The given database statement failed to execute. The error is shown in the
+message.
+
+% DBUTIL_TOO_MANY_ARGUMENTS too many arguments to the command, maximum of one expected
+There were too many command-line arguments to b10-dbutil
+
+% DBUTIL_UPGRADE_CANCELED upgrade canceled; database has not been changed
+The user aborted the upgrade, and b10-dbutil will now exit.
+
+% DBUTIL_UPGRADE_DBUTIL please get the latest version of b10-dbutil and re-run
+A database schema was found that was newer than this version of dbutil, which
+is apparently out of date and should be upgraded itself.
+
+% DBUTIL_UPGRADE_FAILED upgrade failed: %1
+While the upgrade was in progress, an unexpected error occurred. The error
+is shown in the message.
+
+% DBUTIL_UPGRADE_NOT_ATTEMPTED database upgrade was not attempted
+Due to the earlier failure, the database schema upgrade was not attempted,
+and b10-dbutil will now exit.
+
+% DBUTIL_UPGRADE_NOT_NEEDED database already at latest version, no upgrade necessary
+b10-dbutil was told to upgrade the database schema, but it is already at the
+latest version.
+
+% DBUTIL_UPGRADE_NOT_POSSIBLE database at a later version than this utility can support
+b10-dbutil was told to upgrade the database schema, but it is at a higher
+version than this tool currently supports. Please update b10-dbutil and try
+again.
+
+% DBUTIL_UPGRADE_PREPARATION_FAILED upgrade preparation failed: %1
+An unexpected error occurred while b10-dbutil was preparing to upgrade the
+database schema. The error is shown in the message
+
+% DBUTIL_UPGRADE_SUCCESFUL database upgrade successfully completed
+The database schema update was completed successfully.
+
+% DBUTIL_UPGRADING upgrading database from %1 to %2
+An upgrade is in progress, the versions of the current upgrade action are shown.
+
+% DBUTIL_VERSION_CURRENT database version %1
+The current version of the database schema.
+
+% DBUTIL_VERSION_HIGH database is at a later version (%1) than this program can cope with (%2)
+The database schema is at a higher version than b10-dbutil knows about.
+
+% DBUTIL_VERSION_LOW database version %1, latest version is %2.
+The database schema is not up to date, the current version and the latest
+version are in the message.
diff --git a/src/bin/dbutil/run_dbutil.sh.in b/src/bin/dbutil/run_dbutil.sh.in
new file mode 100755
index 0000000000..fea7482cbb
--- /dev/null
+++ b/src/bin/dbutil/run_dbutil.sh.in
@@ -0,0 +1,40 @@
+#! /bin/sh
+
+# Copyright (C) 2010 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
+export PYTHON_EXEC
+
+DBUTIL_PATH=@abs_top_builddir@/src/bin/dbutil
+
+PYTHONPATH=@abs_top_srcdir@/src/bin:@abs_top_builddir@/src/lib/python/isc/log_messages:@abs_top_builddir@/src/lib/python:@abs_top_builddir@/src/bin:@abs_top_srcdir@/src/lib/python
+export PYTHONPATH
+
+# If necessary (rare cases), explicitly specify paths to dynamic libraries
+# required by loadable python modules.
+SET_ENV_LIBRARY_PATH=@SET_ENV_LIBRARY_PATH@
+if test $SET_ENV_LIBRARY_PATH = yes; then
+ @ENV_LIBRARY_PATH@=@abs_top_builddir@/src/lib/dns/.libs:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/cryptolink/.libs:@abs_top_builddir@/src/lib/cc/.libs:@abs_top_builddir@/src/lib/config/.libs:@abs_top_builddir@/src/lib/log/.libs:@abs_top_builddir@/src/lib/util/.libs:@abs_top_builddir@/src/lib/util/io/.libs:@abs_top_builddir@/src/lib/exceptions/.libs:@abs_top_builddir@/src/lib/datasrc/.libs:$@ENV_LIBRARY_PATH@
+ export @ENV_LIBRARY_PATH@
+fi
+
+B10_FROM_SOURCE=@abs_top_srcdir@
+export B10_FROM_SOURCE
+
+BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
+export BIND10_MSGQ_SOCKET_FILE
+
+exec ${PYTHON_EXEC} -O ${DBUTIL_PATH}/b10-dbutil "$@"
diff --git a/src/bin/dbutil/tests/Makefile.am b/src/bin/dbutil/tests/Makefile.am
new file mode 100644
index 0000000000..c03b262aa2
--- /dev/null
+++ b/src/bin/dbutil/tests/Makefile.am
@@ -0,0 +1,6 @@
+SUBDIRS = . testdata
+
+# Tests of the update script.
+
+check-local:
+ $(SHELL) $(abs_builddir)/dbutil_test.sh
diff --git a/src/bin/dbutil/tests/dbutil_test.sh.in b/src/bin/dbutil/tests/dbutil_test.sh.in
new file mode 100755
index 0000000000..92c5953f94
--- /dev/null
+++ b/src/bin/dbutil/tests/dbutil_test.sh.in
@@ -0,0 +1,482 @@
+#!/bin/sh
+# Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+# Checks that the logger will limit the output of messages less severe than
+# the severity/debug setting.
+
+testname="Database Upgrade Test"
+echo $testname
+
+failcount=0
+tempfile=@abs_builddir@/dbutil_test_tempfile_$$
+backupfile=${tempfile}.backup
+testdata=@abs_srcdir@/testdata
+verfile=@abs_builddir@/dbutil_test_verfile_$$
+
+# @brief Record a success
+succeed() {
+ echo "--- PASS"
+}
+
+
+# @brief Record a fail
+#
+# @param $1 Optional additional reason to output
+fail() {
+ if [ "$1" != "" ]
+ then
+ echo "ERROR: $1"
+ fi
+ echo "*** FAIL"
+ failcount=`expr $failcount + 1`
+}
+
+
+# @brief Record a pass if the argument is zero
+#
+# @param $1 Value to test
+passzero() {
+ if [ $1 -eq 0 ]; then
+ succeed
+ else
+ fail
+ fi
+}
+
+
+# @brief Record a fail if the argument is non-zero
+#
+# @param $1 Value to test
+failzero() {
+ if [ $1 -ne 0 ]; then
+ succeed
+ else
+ fail
+ fi
+}
+
+
+# @brief Copy File
+#
+# Executes a "cp" operation followed by a "chmod" to make the target writeable.
+#
+# @param $1 Source file
+# @param $2 Target file
+copy_file () {
+ cp $1 $2
+ chmod a+w $2
+}
+
+
+
+# @brief Check backup file
+#
+# Record a failure if the backup file does not exist or if it is different
+# to the data file. (N.B. No success is recorded if they are the same.)
+#
+# @param $1 Source database file
+# @param $2 Backup file
+check_backup() {
+ if [ ! -e $1 ]
+ then
+ fail "database file $1 not found"
+
+ elif [ ! -e $2 ]
+ then
+ fail "backup file $2 not found"
+
+ else
+ diff $1 $2 > /dev/null
+ if [ $? -ne 0 ]
+ then
+ fail "database file $1 different to backup file $2"
+ fi
+ fi
+}
+
+
+# @brief Check No Backup File
+#
+# Record a failure if the backup file exists. (N.B. No success is recorded if
+# it does not.)
+#
+# @param $1 Source database file (unused, present for symmetry)
+# @param $2 Backup file
+check_no_backup() {
+ if [ -e $2 ]
+ then
+ fail "backup of database $2 exists when it should not"
+ fi
+}
+
+
+# @brief Get Database Schema
+#
+# As the schema stored in the database is format-dependent - how it is printed
+# depends on how the commands were entered (on one line, split across two
+# lines etc.) - comparing schema is awkward.
+#
+# The function sets the local variable db_schema to the output of the
+# .schema command, with spaces removed and upper converted to lowercase.
+#
+# The database is copied before the schema is taken (and removed after)
+# as SQLite3 assummes a writeable database, which may not be the case if
+# getting the schema from a reference copy.
+#
+# @param $1 Database for which the schema is required
+get_schema() {
+ db1=@abs_builddir@/dbutil_test_schema_$$
+ copy_file $1 $db1
+
+ db_schema=`sqlite3 $db1 '.schema' | \
+ awk '{line = line $0} END {print line}' | \
+ sed -e 's/ //g' | \
+ tr [:upper:] [:lower:]`
+ rm -f $db1
+}
+
+
+# @brief Successful Schema Upgrade Test
+#
+# This test is done where the upgrade is expected to be successful - when
+# the end result of the test is that the test database is upgraded to a
+# database of the expected schema.
+#
+# Note: the caller must ensure that $tempfile and $backupfile do not exist
+# on entry, and is responsible for removing them afterwards.
+#
+# @param $1 Database to upgrade
+# @param $2 Expected backup file
+upgrade_ok_test() {
+ copy_file $1 $tempfile
+ ../run_dbutil.sh --upgrade --noconfirm $tempfile
+ if [ $? -eq 0 ]
+ then
+ # Compare schema with the reference
+ get_schema $testdata/v2_0.sqlite3
+ expected_schema=$db_schema
+ get_schema $tempfile
+ actual_schema=$db_schema
+ if [ "$expected_schema" = "$actual_schema" ]
+ then
+ succeed
+ else
+ fail "upgraded schema not as expected"
+ fi
+
+ # Check the version is set correctly
+ check_version $tempfile "V2.0"
+
+ # Check that a backup was made
+ check_backup $1 $2
+ else
+ # Error should have been output already
+ fail
+ fi
+}
+
+
+# @brief Unsuccessful Upgrade Test
+#
+# Checks that an upgrade of the specified database fails.
+#
+# Note: the caller must ensure that $tempfile and $backupfile do not exist
+# on entry, and is responsible for removing them afterwards.
+#
+# @param $1 Database to upgrade
+# @param $2 Expected backup file
+upgrade_fail_test() {
+ copy_file $1 $tempfile
+ ../run_dbutil.sh --upgrade --noconfirm $tempfile
+ failzero $?
+ check_backup $1 $backupfile
+}
+
+
+# @brief Record Count Test
+#
+# Checks that the count of records in each table is preserved in the upgrade.
+#
+# Note 1: This test assumes that the "diffs" table is present.
+# Note 2: The caller must ensure that $tempfile and $backupfile do not exist
+# on entry, and is responsible for removing them afterwards.
+#
+# @brief $1 Database to upgrade
+record_count_test() {
+ copy_file $1 $tempfile
+
+ diffs_count=`sqlite3 $tempfile 'select count(*) from diffs'`
+ nsec3_count=`sqlite3 $tempfile 'select count(*) from nsec3'`
+ records_count=`sqlite3 $tempfile 'select count(*) from records'`
+ zones_count=`sqlite3 $tempfile 'select count(*) from zones'`
+
+ ../run_dbutil.sh --upgrade --noconfirm $tempfile
+ if [ $? -ne 0 ]
+ then
+ # Reason for failure should already have been output
+ fail
+ else
+ new_diffs_count=`sqlite3 $tempfile 'select count(*) from diffs'`
+ new_nsec3_count=`sqlite3 $tempfile 'select count(*) from nsec3'`
+ new_records_count=`sqlite3 $tempfile 'select count(*) from records'`
+ new_zones_count=`sqlite3 $tempfile 'select count(*) from zones'`
+
+ if [ $diffs_count -ne $new_diffs_count ]
+ then
+ fail "diffs table was not completely copied"
+ fi
+
+ if [ $nsec3_count -ne $new_nsec3_count ]
+ then
+ fail "nsec3 table was not completely copied"
+ fi
+
+ if [ $records_count -ne $new_records_count ]
+ then
+ fail "records table was not completely copied"
+ fi
+
+ if [ $zones_count -ne $new_zones_count ]
+ then
+ fail "zones table was not completely copied"
+ fi
+
+ # As an extra check, test that the backup was successful
+ check_backup $1 $backupfile
+ fi
+}
+
+
+# @brief Version Check
+#
+# Checks that the database is at the specified version (and so checks the
+# --check function). On success, a pass is recorded.
+#
+# @param $1 Database to check
+# @param $2 Expected version string
+check_version() {
+ copy_file $1 $verfile
+ ../run_dbutil.sh --check $verfile
+ if [ $? -gt 2 ]
+ then
+ fail "version check failed on database $1; return code $?"
+ else
+ ../run_dbutil.sh --check $verfile 2>&1 | grep "$2" > /dev/null
+ if [ $? -ne 0 ]
+ then
+ fail "database $1 not at expected version $2 (output: $?)"
+ else
+ succeed
+ fi
+ fi
+ rm -f $verfile
+}
+
+
+# @brief Version Check Fail
+#
+# Does a version check but expected the check to fail
+#
+# @param $1 Database to check
+# @param $2 Backup file
+check_version_fail() {
+ copy_file $1 $verfile
+ ../run_dbutil.sh --check $verfile
+ failzero $?
+ check_no_backup $tempfile $backupfile
+}
+
+
+# Main test sequence
+
+rm -f $tempfile $backupfile
+
+# Test 1 - check that the utility fails if the database does not exist
+echo "1.1. Non-existent database - check"
+../run_dbutil.sh --check $tempfile
+failzero $?
+check_no_backup $tempfile $backupfile
+
+echo "1.2. Non-existent database - upgrade"
+../run_dbutil.sh --upgrade --noconfirm $tempfile
+failzero $?
+check_no_backup $tempfile $backupfile
+rm -f $tempfile $backupfile
+
+
+# Test 2 - should fail to check an empty file and fail to upgrade it
+echo "2.1. Database is an empty file - check"
+touch $tempfile
+check_version_fail $tempfile $backupfile
+rm -f $tempfile $backupfile
+
+echo "2.2. Database is an empty file - upgrade"
+touch $tempfile
+../run_dbutil.sh --upgrade --noconfirm $tempfile
+failzero $?
+# A backup is performed before anything else, so the backup should exist.
+check_backup $tempfile $backupfile
+rm -f $tempfile $backupfile
+
+
+echo "3.1. Database is not an SQLite file - check"
+echo "This is not an sqlite3 database" > $tempfile
+check_version_fail $tempfile $backupfile
+rm -f $tempfile $backupfile
+
+echo "3.2. Database is not an SQLite file - upgrade"
+echo "This is not an sqlite3 database" > $tempfile
+../run_dbutil.sh --upgrade --noconfirm $tempfile
+failzero $?
+# ...and as before, a backup should have been created
+check_backup $tempfile $backupfile
+rm -f $tempfile $backupfile
+
+
+echo "4.1. Database is an SQLite3 file without the schema table - check"
+check_version_fail $testdata/no_schema.sqlite3 $backupfile
+rm -f $tempfile $backupfile
+
+echo "4.1. Database is an SQLite3 file without the schema table - upgrade"
+upgrade_fail_test $testdata/no_schema.sqlite3 $backupfile
+rm -f $tempfile $backupfile
+
+
+echo "5.1. Database is an old V1 database - check"
+check_version $testdata/old_v1.sqlite3 "V1.0"
+check_no_backup $tempfile $backupfile
+rm -f $tempfile $backupfile
+
+echo "5.2. Database is an old V1 database - upgrade"
+upgrade_ok_test $testdata/old_v1.sqlite3 $backupfile
+rm -f $tempfile $backupfile
+
+
+echo "6.1. Database is new V1 database - check"
+check_version $testdata/new_v1.sqlite3 "V1.0"
+check_no_backup $tempfile $backupfile
+rm -f $tempfile $backupfile
+
+echo "6.2. Database is a new V1 database - upgrade"
+upgrade_ok_test $testdata/new_v1.sqlite3 $backupfile
+rm -f $tempfile $backupfile
+
+
+echo "7.1. Database is V2.0 database - check"
+check_version $testdata/v2_0.sqlite3 "V2.0"
+check_no_backup $tempfile $backupfile
+rm -f $tempfile $backupfile
+
+echo "7.2. Database is a V2.0 database - upgrade"
+upgrade_ok_test $testdata/v2_0.sqlite3 $backupfile
+rm -f $tempfile $backupfile
+
+
+echo "8.1. Database is V2.0 database with empty schema table - check"
+check_version_fail $testdata/empty_version.sqlite3 $backupfile
+rm -f $tempfile $backupfile
+
+echo "8.2. Database is V2.0 database with empty schema table - upgrade"
+upgrade_fail_test $testdata/empty_version.sqlite3 $backupfile
+rm -f $tempfile $backupfile
+
+
+echo "9.1. Database is V2.0 database with over-full schema table - check"
+check_version_fail $testdata/too_many_version.sqlite3 $backupfile
+rm -f $tempfile $backupfile
+
+echo "9.2. Database is V2.0 database with over-full schema table - upgrade"
+upgrade_fail_test $testdata/too_many_version.sqlite3 $backupfile
+rm -f $tempfile $backupfile
+
+
+echo "10.0. Upgrade corrupt database"
+upgrade_fail_test $testdata/corrupt.sqlite3 $backupfile
+rm -f $tempfile $backupfile
+
+
+echo "11. Record count test"
+record_count_test $testdata/new_v1.sqlite3
+rm -f $tempfile $backupfile
+
+
+echo "12. Backup file already exists"
+touch $backupfile
+touch ${backupfile}-1
+upgrade_ok_test $testdata/v2_0.sqlite3 ${backupfile}-2
+rm -f $tempfile $backupfile ${backupfile}-1 ${backupfile}-2
+
+
+echo "13.1 Command-line errors"
+copy_file $testdata/old_v1.sqlite3 $tempfile
+../run_dbutil.sh $tempfile
+failzero $?
+../run_dbutil.sh --upgrade --check $tempfile
+failzero $?
+../run_dbutil.sh --noconfirm --check $tempfile
+failzero $?
+../run_dbutil.sh --check
+failzero $?
+../run_dbutil.sh --upgrade --noconfirm
+failzero $?
+../run_dbutil.sh --check $tempfile $backupfile
+failzero $?
+../run_dbutil.sh --upgrade --noconfirm $tempfile $backupfile
+failzero $?
+rm -f $tempfile $backupfile
+
+echo "13.2 verbose flag"
+copy_file $testdata/old_v1.sqlite3 $tempfile
+../run_dbutil.sh --upgrade --noconfirm --verbose $tempfile
+passzero $?
+rm -f $tempfile $backupfile
+
+echo "13.3 quiet flag"
+copy_file $testdata/old_v1.sqlite3 $tempfile
+../run_dbutil.sh --check --quiet $tempfile 2>&1 | grep .
+failzero $?
+rm -f $tempfile $backupfile
+
+echo "13.3 Interactive prompt - yes"
+copy_file $testdata/old_v1.sqlite3 $tempfile
+../run_dbutil.sh --upgrade $tempfile << .
+Yes
+.
+passzero $?
+check_version $tempfile "V2.0"
+rm -f $tempfile $backupfile
+
+echo "13.4 Interactive prompt - no"
+copy_file $testdata/old_v1.sqlite3 $tempfile
+../run_dbutil.sh --upgrade $tempfile << .
+no
+.
+passzero $?
+diff $testdata/old_v1.sqlite3 $tempfile > /dev/null
+passzero $?
+rm -f $tempfile $backupfile
+
+
+# Report the result
+if [ $failcount -eq 0 ]; then
+ echo "PASS: $testname"
+elif [ $failcount -eq 1 ]; then
+ echo "FAIL: $testname - 1 test failed"
+else
+ echo "FAIL: $testname - $failcount tests failed"
+fi
+
+# Exit with appropriate error status
+exit $failcount
diff --git a/src/bin/dbutil/tests/testdata/Makefile.am b/src/bin/dbutil/tests/testdata/Makefile.am
new file mode 100644
index 0000000000..0d850a70f0
--- /dev/null
+++ b/src/bin/dbutil/tests/testdata/Makefile.am
@@ -0,0 +1,12 @@
+EXTRA_DIST =
+EXTRA_DIST += corrupt.sqlite3
+EXTRA_DIST += empty_schema.sqlite3
+EXTRA_DIST += empty_v1.sqlite3
+EXTRA_DIST += empty_version.sqlite3
+EXTRA_DIST += invalid_v1.sqlite3
+EXTRA_DIST += new_v1.sqlite3
+EXTRA_DIST += no_schema.sqlite3
+EXTRA_DIST += old_v1.sqlite3
+EXTRA_DIST += README
+EXTRA_DIST += too_many_version.sqlite3
+EXTRA_DIST += v2_0.sqlite3
diff --git a/src/bin/dbutil/tests/testdata/README b/src/bin/dbutil/tests/testdata/README
new file mode 100644
index 0000000000..83ce01f550
--- /dev/null
+++ b/src/bin/dbutil/tests/testdata/README
@@ -0,0 +1,41 @@
+The versioning of BIND 10 databases to date has not been the best:
+
+The original database is known here as the "old V1" schema. It had a
+schema_version table, with the single "version" value set to 1.
+
+The schema was then updated with a "diffs" table. This is referred to
+here as the "new V1" schema.
+
+The Spring 2012 release of BIND 10 modified the schema. The
+schema_version table was updated to include a "minor" column, holding the
+minor version number. Other changes to the database included redefining
+"STRING" columns as "TEXT" columns. This is referred to as the "V2.0
+schema".
+
+The following test data files are present:
+
+empty_schema.sqlite3: A database conforming to the new V1 schema.
+However, there is nothing in the schema_version table.
+
+empty_v1.sqlite3: A database conforming to the new V1 schema.
+The database is empty, except for the schema_version table, where the
+"version" column is set to 1.
+
+empty_version.sqlite3: A database conforming to the V2.0 schema but without
+anything in the schema_version table.
+
+no_schema.sqlite3: A valid SQLite3 database, but without a schema_version
+table.
+
+old_v1.sqlite3: A valid SQLite3 database conforming to the old V1 schema.
+It does not have a diffs table.
+
+invalid_v1.sqlite3: A valid SQLite3 database that, although the schema
+is marked as V1, does not have the nsec3 table.
+
+new_v1.sqlite3: A valid SQLite3 database with data in all the tables
+(although the single rows in both the nsec3 and diffs table make no
+sense, but are valid).
+
+too_many_version.sqlite3: A database conforming to the V2.0 schema but with
+too many rows of data.
diff --git a/src/bin/dbutil/tests/testdata/corrupt.sqlite3 b/src/bin/dbutil/tests/testdata/corrupt.sqlite3
new file mode 100644
index 0000000000..69683b7477
Binary files /dev/null and b/src/bin/dbutil/tests/testdata/corrupt.sqlite3 differ
diff --git a/src/bin/dbutil/tests/testdata/empty_schema.sqlite3 b/src/bin/dbutil/tests/testdata/empty_schema.sqlite3
new file mode 100644
index 0000000000..b8031490a9
Binary files /dev/null and b/src/bin/dbutil/tests/testdata/empty_schema.sqlite3 differ
diff --git a/src/bin/dbutil/tests/testdata/empty_v1.sqlite3 b/src/bin/dbutil/tests/testdata/empty_v1.sqlite3
new file mode 100644
index 0000000000..5ad21363ce
Binary files /dev/null and b/src/bin/dbutil/tests/testdata/empty_v1.sqlite3 differ
diff --git a/src/bin/dbutil/tests/testdata/empty_version.sqlite3 b/src/bin/dbutil/tests/testdata/empty_version.sqlite3
new file mode 100644
index 0000000000..b820fa9990
Binary files /dev/null and b/src/bin/dbutil/tests/testdata/empty_version.sqlite3 differ
diff --git a/src/bin/dbutil/tests/testdata/invalid_v1.sqlite3 b/src/bin/dbutil/tests/testdata/invalid_v1.sqlite3
new file mode 100644
index 0000000000..e411fd088a
Binary files /dev/null and b/src/bin/dbutil/tests/testdata/invalid_v1.sqlite3 differ
diff --git a/src/bin/dbutil/tests/testdata/new_v1.sqlite3 b/src/bin/dbutil/tests/testdata/new_v1.sqlite3
new file mode 100644
index 0000000000..9a885a4816
Binary files /dev/null and b/src/bin/dbutil/tests/testdata/new_v1.sqlite3 differ
diff --git a/src/bin/dbutil/tests/testdata/no_schema.sqlite3 b/src/bin/dbutil/tests/testdata/no_schema.sqlite3
new file mode 100644
index 0000000000..9dd06144e3
Binary files /dev/null and b/src/bin/dbutil/tests/testdata/no_schema.sqlite3 differ
diff --git a/src/bin/dbutil/tests/testdata/old_v1.sqlite3 b/src/bin/dbutil/tests/testdata/old_v1.sqlite3
new file mode 100644
index 0000000000..32dbb9b35b
Binary files /dev/null and b/src/bin/dbutil/tests/testdata/old_v1.sqlite3 differ
diff --git a/src/bin/dbutil/tests/testdata/too_many_version.sqlite3 b/src/bin/dbutil/tests/testdata/too_many_version.sqlite3
new file mode 100644
index 0000000000..5dc8ae343f
Binary files /dev/null and b/src/bin/dbutil/tests/testdata/too_many_version.sqlite3 differ
diff --git a/src/bin/dbutil/tests/testdata/v2_0.sqlite3 b/src/bin/dbutil/tests/testdata/v2_0.sqlite3
new file mode 100644
index 0000000000..18784fdea3
Binary files /dev/null and b/src/bin/dbutil/tests/testdata/v2_0.sqlite3 differ
diff --git a/src/bin/xfrout/b10-xfrout.8 b/src/bin/xfrout/b10-xfrout.8
index 483e2c05f8..b3200b588f 100644
--- a/src/bin/xfrout/b10-xfrout.8
+++ b/src/bin/xfrout/b10-xfrout.8
@@ -9,6 +9,15 @@
.\"
.TH "B10\-XFROUT" "8" "March 16\&. 2012" "BIND10" "BIND10"
.\" -----------------------------------------------------------------
+.\" * Define some portability stuff
+.\" -----------------------------------------------------------------
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.\" http://bugs.debian.org/507673
+.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.\" -----------------------------------------------------------------
.\" * set default formatting
.\" -----------------------------------------------------------------
.\" disable hyphenation
diff --git a/src/lib/python/isc/log_messages/Makefile.am b/src/lib/python/isc/log_messages/Makefile.am
index 4b7e1d1422..552f5d9e0a 100644
--- a/src/lib/python/isc/log_messages/Makefile.am
+++ b/src/lib/python/isc/log_messages/Makefile.am
@@ -14,6 +14,7 @@ EXTRA_DIST += config_messages.py
EXTRA_DIST += notify_out_messages.py
EXTRA_DIST += libxfrin_messages.py
EXTRA_DIST += server_common_messages.py
+EXTRA_DIST += dbutil_messages.py
CLEANFILES = __init__.pyc
CLEANFILES += bind10_messages.pyc
diff --git a/src/lib/python/isc/log_messages/dbutil_messages.py b/src/lib/python/isc/log_messages/dbutil_messages.py
new file mode 100644
index 0000000000..c06dfef6cf
--- /dev/null
+++ b/src/lib/python/isc/log_messages/dbutil_messages.py
@@ -0,0 +1 @@
+from work.dbutil_messages import *