123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617 |
- #!@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"
- ]
- },
- {'from': (2, 0), 'to': (2, 1),
- 'statements': [
- # Enforce that only one NSEC3 RR exists for an owner name in
- # the zone.
- "CREATE UNIQUE INDEX nsec3_by_zoneid_and_owner ON nsec3 " +
- "(zone_id, owner)"
- ]
- }
- # To extend this, leave the above statements in place and add another
- # dictionary to the list. The "from" version should be (2, 1), 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)
|