mirror of
https://gitlab.isc.org/isc-projects/kea
synced 2025-08-29 21:18:02 +00:00
[963] Dabase utility program and tests
This commit is contained in:
parent
823f30307b
commit
2e06048a76
@ -994,6 +994,8 @@ 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/host/Makefile
|
||||
src/bin/loadzone/Makefile
|
||||
src/bin/loadzone/tests/correct/Makefile
|
||||
@ -1007,8 +1009,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
|
||||
@ -1122,6 +1124,8 @@ 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/tests/dbutil_test.sh
|
||||
src/bin/ddns/ddns.py
|
||||
src/bin/xfrin/tests/xfrin_test
|
||||
src/bin/xfrin/xfrin.py
|
||||
@ -1205,6 +1209,7 @@ 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/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
|
||||
|
14
src/bin/dbutil/Makefile.am
Normal file
14
src/bin/dbutil/Makefile.am
Normal file
@ -0,0 +1,14 @@
|
||||
SUBDIRS = . tests
|
||||
|
||||
bin_SCRIPTS = b10-dbutil
|
||||
|
||||
CLEANFILES = b10-dbutil b10-dbutil.pyc
|
||||
|
||||
b10-dbutil: dbutil.py
|
||||
$(SED) -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" dbutil.py > $@
|
||||
chmod a+x $@
|
||||
|
||||
CLEANDIRS = __pycache__
|
||||
|
||||
clean-local:
|
||||
rm -rf $(CLEANDIRS)
|
561
src/bin/dbutil/dbutil.py.in
Executable file
561
src/bin/dbutil/dbutil.py.in
Executable file
@ -0,0 +1,561 @@
|
||||
#!@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 [database]
|
||||
# b10-dbutil --upgrade [--noconfirm] [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. In both cases, if the databas
|
||||
# file is not given on the command line, the default database will be accessed.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import os, sqlite3, shutil, sys
|
||||
from optparse import OptionParser
|
||||
|
||||
# Default database to use if the database is not given on the command line.
|
||||
# (This is the same string as in "auth.spec.pre.in".)
|
||||
default_database_file = "@@LOCALSTATEDIR@@/@PACKAGE@/zone.sqlite3"
|
||||
|
||||
# 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.
|
||||
]
|
||||
|
||||
# Exception class to indicate error exit
|
||||
class DbutilException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def info(text):
|
||||
"""
|
||||
@brief Write informational message to stdout.
|
||||
"""
|
||||
sys.stdout.write("INFO: " + text + "\n")
|
||||
|
||||
|
||||
# @brief Database Encapsulation
|
||||
#
|
||||
# Encapsulates the SQL database, both the connection and the cursor. The
|
||||
# methods will cause a program exit on any error.
|
||||
class Database:
|
||||
def __init__(self, db_file, verbose = False):
|
||||
"""
|
||||
@brief Constructor
|
||||
|
||||
@param db_file Name of the database file
|
||||
@param verbose If True, print all SQL statements to stdout before
|
||||
executing them.
|
||||
"""
|
||||
self.connection = None
|
||||
self.cursor = None
|
||||
self.db_file = db_file
|
||||
self.backup_file = None
|
||||
self.verbose = verbose
|
||||
|
||||
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, what = None):
|
||||
"""
|
||||
@brief Execute Statement
|
||||
|
||||
Executes the given statement, exiting the program on error. If
|
||||
verbose mode is set, the statement is printed to stdout before
|
||||
execution.
|
||||
|
||||
@param statement SQL statement to execute
|
||||
@param what Reason for the action (used in the error message if the
|
||||
action fails)
|
||||
"""
|
||||
if self.verbose:
|
||||
sys.stdout.write(statement + "\n")
|
||||
|
||||
try:
|
||||
self.cursor.execute(statement)
|
||||
except Exception as ex:
|
||||
if (what is None):
|
||||
raise DbutilException("SQL Error - " + str(ex))
|
||||
else:
|
||||
raise DbutilException("failed to " + what + " - " + 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)
|
||||
info("database " + self.db_file + " backed up to " + 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 database to the latest version.
|
||||
|
||||
The utility will take a copy of the database file before running so, in the
|
||||
unlikely event of a problem, you will be able to restore the database from
|
||||
the backup. To ensure that the integrity of this backup, please ensure that
|
||||
BIND 10 is not running before proceeding.
|
||||
""")
|
||||
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: ")
|
||||
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 get_latest_version():
|
||||
"""
|
||||
@brief Returns the latest version of 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)
|
||||
"""
|
||||
|
||||
# Check only one row of data in the version table.
|
||||
db.execute("SELECT COUNT(*) FROM schema_version", "get database version")
|
||||
result = db.result()
|
||||
if result[0] == 0:
|
||||
raise DbutilException("unable to determine database version - " +
|
||||
"nothing in schema_version table")
|
||||
elif result[0] > 1:
|
||||
raise DbutilException("unable to determine database version - " +
|
||||
"too many rows in schema_version table")
|
||||
|
||||
# Get the version information.
|
||||
db.execute("SELECT * FROM schema_version", "get database version")
|
||||
result = db.result()
|
||||
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]
|
||||
|
||||
return (major, minor)
|
||||
|
||||
|
||||
def match_version(db, expected):
|
||||
"""
|
||||
@brief Check database version against that expected
|
||||
|
||||
Checks whether the version of the database matches that expected for
|
||||
the upgrade. Both the major and minor versions must match.
|
||||
|
||||
@param db Database
|
||||
@param expected Expected version of the database in form (major, minor)
|
||||
|
||||
@return True if the versions match, false if they don't.
|
||||
"""
|
||||
current = get_version(db)
|
||||
return expected == current
|
||||
|
||||
|
||||
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".
|
||||
"""
|
||||
increment = (version_string(upgrade['from']) + " to " +
|
||||
version_string(upgrade['to']))
|
||||
action = "upgrading database from " + increment
|
||||
info(action)
|
||||
for statement in upgrade['statements']:
|
||||
db.execute(statement, "upgrade database from " + increment)
|
||||
|
||||
# Update the version information
|
||||
db.execute("DELETE FROM schema_version", "update version information")
|
||||
db.execute("INSERT INTO schema_version VALUES (" +
|
||||
str(upgrade['to'][0]) + "," + str(upgrade['to'][1]) + ")",
|
||||
"update version information")
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
if match_version(db, get_latest_version()):
|
||||
info("database already at latest version, no upgrade necessary")
|
||||
|
||||
else:
|
||||
# Work our way through all upgrade increments
|
||||
count = 0
|
||||
for upgrade in upgrades:
|
||||
if match_version(db, upgrade['from']):
|
||||
perform_upgrade(db, upgrade)
|
||||
count = count + 1
|
||||
|
||||
if count > 0:
|
||||
info("database upgrade successfully completed")
|
||||
else:
|
||||
# Should not get here, as we established earlier that the database
|
||||
# was not at the latest version so we should have upgraded.
|
||||
# (Although it is possible that as version checks are for equality,
|
||||
# an older version of dbutil was being run against a newer version
|
||||
# of the database.)
|
||||
raise DbutilException("database not at latest version but no " +
|
||||
"upgrade was performed")
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
current = get_version(db);
|
||||
latest = get_latest_version()
|
||||
|
||||
if current == latest:
|
||||
info("database version " + version_string(current))
|
||||
info("this is the latest version of the database schema, " +
|
||||
"no upgrade is required")
|
||||
else:
|
||||
info("database version " + version_string(current) +
|
||||
", latest version is " + version_string(latest))
|
||||
info("re-run this program with the --upgrade switch to upgrade")
|
||||
|
||||
|
||||
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)
|
||||
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")
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
# Set the database file on which to operate
|
||||
if (len(args) > 1):
|
||||
sys.stderr.write(usage + "\n")
|
||||
sys.exit(1)
|
||||
elif len(args) == 0:
|
||||
args.append(default_database_file)
|
||||
|
||||
# Check for conflicting options. If some are found, output a suitable
|
||||
# error message and print the usage.
|
||||
if options.check and options.upgrade:
|
||||
sys.stderr.write("cannot select both --check and --upgrade, " +
|
||||
"please choose one")
|
||||
elif (not options.check) and (not options.upgrade):
|
||||
sys.stderr.write("must select one of --check or --upgrade")
|
||||
elif (options.check and options.noconfirm):
|
||||
sys.stderr.write("--noconfirm is not compatible with --check")
|
||||
else:
|
||||
return (options, args)
|
||||
|
||||
# Only get here on conflicting options
|
||||
parser.print_usage()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
(options, args) = parse_command()
|
||||
db = Database(args[0], options.verbose)
|
||||
|
||||
if options.check:
|
||||
# Check database - open, report, and close
|
||||
try:
|
||||
db.open()
|
||||
check_version(db)
|
||||
db.close()
|
||||
except Exception as ex:
|
||||
sys.stderr.write("ERROR: unable to check database version - " +
|
||||
str(ex) + "\n")
|
||||
sys.exit(1)
|
||||
|
||||
elif options.upgrade:
|
||||
# Upgrade. Check if this is what they really want to do
|
||||
if not options.noconfirm:
|
||||
proceed = prompt_user()
|
||||
if not proceed:
|
||||
info("upgrade abandoned - database has not been changed\n")
|
||||
sys.exit(0)
|
||||
|
||||
# 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:
|
||||
sys.stderr.write("ERROR: upgrade failed - " + str(ex) + "\n")
|
||||
sys.stderr.write("WARN: database may be corrupt, " +
|
||||
"restore database from backup\n")
|
||||
else:
|
||||
sys.stderr.write("ERROR: upgrade preparation failed - " +
|
||||
str(ex) + "\n")
|
||||
sys.stderr.write("INFO: database upgrade was not attempted\n")
|
||||
sys.exit(1)
|
||||
else:
|
||||
sys.stderr.write("ERROR: internal error, neither --check nor " +
|
||||
" --upgrade selected")
|
||||
sys.exit(1)
|
6
src/bin/dbutil/tests/Makefile.am
Normal file
6
src/bin/dbutil/tests/Makefile.am
Normal file
@ -0,0 +1,6 @@
|
||||
SUBDIRS = .
|
||||
|
||||
# Tests of the update script.
|
||||
|
||||
check-local:
|
||||
$(SHELL) $(abs_builddir)/dbutil_test.sh
|
409
src/bin/dbutil/tests/dbutil_test.sh.in
Executable file
409
src/bin/dbutil/tests/dbutil_test.sh.in
Executable file
@ -0,0 +1,409 @@
|
||||
#!/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 [ "x$1" != "x" ]
|
||||
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 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_$$
|
||||
cp $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
|
||||
upgrade_ok_test() {
|
||||
cp $1 $tempfile
|
||||
../b10-dbutil --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 [ x$expected_schema = x$actual_schema ]
|
||||
then
|
||||
succeed
|
||||
else
|
||||
fail "upgraded schema not as expected"
|
||||
fi
|
||||
|
||||
# and check the version is set correctly
|
||||
check_version $tempfile "V2.0"
|
||||
else
|
||||
# Error should have been output already
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# @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() {
|
||||
cp $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'`
|
||||
|
||||
../b10-dbutil --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() {
|
||||
cp $1 $verfile
|
||||
../b10-dbutil --check $verfile
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
fail "version check failed on database $1"
|
||||
else
|
||||
../b10-dbutil --check $verfile | grep "$2"
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
fail "database $1 not at expected version $2"
|
||||
else
|
||||
succeed
|
||||
fi
|
||||
fi
|
||||
rm -f $verfile
|
||||
}
|
||||
|
||||
|
||||
# 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"
|
||||
../b10-dbutil --check $tempfile
|
||||
failzero $?
|
||||
check_no_backup $tempfile $backupfile
|
||||
|
||||
echo "1.2. Non-existent database - upgrade"
|
||||
../b10-dbutil --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
|
||||
../b10-dbutil --check $tempfile
|
||||
failzero $?
|
||||
check_no_backup $tempfile $backupfile
|
||||
rm -f $tempfile $backupfile
|
||||
|
||||
echo "2.2. Database is an empty file - upgrade"
|
||||
touch $tempfile
|
||||
../b10-dbutil --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
|
||||
../b10-dbutil --check $tempfile
|
||||
failzero $?
|
||||
check_no_backup $tempfile $backupfile
|
||||
|
||||
echo "3.2. Database is not an SQLite file - upgrade"
|
||||
../b10-dbutil --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"
|
||||
cp $testdata/no_schema.sqlite3 $tempfile
|
||||
../b10-dbutil --check $tempfile
|
||||
failzero $?
|
||||
check_no_backup $tempfile $backupfile
|
||||
rm -f $tempfile $backupfile
|
||||
|
||||
echo "4.1. Database is an SQLite3 file without the schema table - upgrade"
|
||||
cp $testdata/no_schema.sqlite3 $tempfile
|
||||
../b10-dbutil --upgrade --noconfirm $tempfile
|
||||
failzero $?
|
||||
check_backup $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
|
||||
check_backup $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
|
||||
check_backup $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
|
||||
check_backup $testdata/v2_0.sqlite3 $backupfile
|
||||
rm -f $tempfile $backupfile
|
||||
|
||||
|
||||
echo "8. Record count test"
|
||||
record_count_test testdata/new_v1.sqlite3
|
||||
rm -f $tempfile $backupfile
|
||||
|
||||
|
||||
echo "9. Backup file already exists"
|
||||
touch $backupfile
|
||||
touch ${backupfile}-1
|
||||
upgrade_ok_test $testdata/v2_0.sqlite3
|
||||
check_backup $testdata/v2_0.sqlite3 ${backupfile}-2
|
||||
rm -f $tempfile $backupfile ${backupfile}-1 ${backupfile}-2
|
||||
|
||||
|
||||
echo "10.1 Incompatible flags"
|
||||
cp $testdata/old_v1.sqlite3 $tempfile
|
||||
../b10-util --upgrade --check $tempfile
|
||||
failzero $?
|
||||
../b10-util --upgrade --check $tempfile
|
||||
failzero $?
|
||||
../b10-util --noconfirm --check $tempfile
|
||||
failzero $?
|
||||
rm -f $tempfile $backupfile
|
||||
|
||||
echo "10.2 verbose flag"
|
||||
cp $testdata/old_v1.sqlite3 $tempfile
|
||||
../b10-dbutil --upgrade --noconfirm --verbose $tempfile
|
||||
passzero $?
|
||||
rm -f $tempfile $backupfile
|
||||
|
||||
echo "10.3 Interactive prompt - yes"
|
||||
cp $testdata/old_v1.sqlite3 $tempfile
|
||||
../b10-dbutil --upgrade $tempfile << .
|
||||
Yes
|
||||
.
|
||||
passzero $?
|
||||
check_version $tempfile "V2.0"
|
||||
rm -f $tempfile $backupfile
|
||||
|
||||
echo "10.4 Interactive prompt - no"
|
||||
cp $testdata/old_v1.sqlite3 $tempfile
|
||||
../b10-dbutil --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
|
35
src/bin/dbutil/tests/testdata/README
vendored
Normal file
35
src/bin/dbutil/tests/testdata/README
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
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.
|
||||
|
||||
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).
|
BIN
src/bin/dbutil/tests/testdata/empty_schema.sqlite3
vendored
Normal file
BIN
src/bin/dbutil/tests/testdata/empty_schema.sqlite3
vendored
Normal file
Binary file not shown.
BIN
src/bin/dbutil/tests/testdata/empty_v1.sqlite3
vendored
Normal file
BIN
src/bin/dbutil/tests/testdata/empty_v1.sqlite3
vendored
Normal file
Binary file not shown.
BIN
src/bin/dbutil/tests/testdata/invalid_v1.sqlite3
vendored
Normal file
BIN
src/bin/dbutil/tests/testdata/invalid_v1.sqlite3
vendored
Normal file
Binary file not shown.
BIN
src/bin/dbutil/tests/testdata/new_v1.sqlite3
vendored
Normal file
BIN
src/bin/dbutil/tests/testdata/new_v1.sqlite3
vendored
Normal file
Binary file not shown.
BIN
src/bin/dbutil/tests/testdata/no_schema.sqlite3
vendored
Normal file
BIN
src/bin/dbutil/tests/testdata/no_schema.sqlite3
vendored
Normal file
Binary file not shown.
BIN
src/bin/dbutil/tests/testdata/old_v1.sqlite3
vendored
Normal file
BIN
src/bin/dbutil/tests/testdata/old_v1.sqlite3
vendored
Normal file
Binary file not shown.
BIN
src/bin/dbutil/tests/testdata/v2_0.sqlite3
vendored
Normal file
BIN
src/bin/dbutil/tests/testdata/v2_0.sqlite3
vendored
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user