diff options
-rw-r--r-- | rpkid/rpki-sql-upgrade | 187 |
1 files changed, 187 insertions, 0 deletions
diff --git a/rpkid/rpki-sql-upgrade b/rpkid/rpki-sql-upgrade new file mode 100644 index 00000000..18987e82 --- /dev/null +++ b/rpkid/rpki-sql-upgrade @@ -0,0 +1,187 @@ +#!/usr/bin/env python + +# $Id$ +# +# Copyright (C) 2014 Dragon Research Labs ("DRL") +# +# 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 DRL DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL DRL 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. + +""" +Run upgrade scripts to drag installed version of RPKI-related SQL +databases up to the current version. + +We ignore the IRDB, on the theory that the Django stuff can take care +of itself, using South migrations if necessary. We could use South +for everything if we were to rewrit the rpkid and pubd databases using +the Django ORM, but that's a big change. Perhaps someday. +""" + +import os +import sys +import time +import glob +import argparse +import rpki.config +import rpki.autoconf +import rpki.version +import rpki.sundial + +from rpki.mysql_import import MySQLdb, _mysql_exceptions + +ER_NO_SUCH_TABLE = 1146 # See mysqld_ername.h + +class Version(object): + """ + A version number. This is a class in its own right to force the + comparision and string I/O behavior we want. + """ + + def __init__(self, v): + if v is None: + v = "0.0" + self.v = tuple(v.lower().split(".")) + + def __str__(self): + return ".".join(self.v) + + def __cmp__(self, other): + return cmp(self.v, other.v) + +current_version = Version(rpki.version.VERSION) + +class Database(object): + """ + One of the SQL databases we're whacking. + + NB: The SQL definition for the upgrade_version table is embedded in + this class rather than being declared in any of the .sql files. + This is deliberate: nothing but the upgrade system should ever touch + this table, and it's simpler to keep everything in one place. + + We have to be careful about SQL commits here, because CREATE TABLE + implies an automatic commit. So presence of the table per se isn't + significant, only its content (or lack thereof). + """ + + upgrade_version_table_schema = """ + CREATE TABLE upgrade_version ( + version TEXT NOT NULL, + updated DATETIME NOT NULL + ) ENGINE=InnoDB + """ + + def __init__(self, name): + self.name = name + self.enabled = cfg.getboolean("start_" + name, False) + if self.enabled: + if args.verbose: + print "Opening", name + self.db = MySQLdb.connect( + db = cfg.get("sql-database", section = name), + user = cfg.get("sql-username", section = name), + passwd = cfg.get("sql-password", section = name)) + self.db.autocommit(False) + cur = self.db.cursor() + try: + cur.execute("SELECT version FROM upgrade_version") + except _mysql_exceptions.ProgrammingError, e: + if e.args[0] != ER_NO_SUCH_TABLE: + raise + if args.verbose: + print "Creating upgrade_version table" + cur.execute(self.upgrade_version_table_schema) + cur.close() + if args.verbose: + print "Current version of", name, "is", self.version + + @property + def version(self): + if self.enabled: + cur = self.db.cursor() + cur.execute("SELECT version FROM upgrade_version") + v = self.cur.fetchone() + cur.close() + return Version(None if v is None else v[0]) + else: + return current_version + + @version.setter + def version(self, v): + if self.enabled and v > self.version: + cur = self.db.cursor() + cur.execute("DELETE FROM upgrade_version") + cur.execute("INSERT upgrade_version (version, updated) VALUES (%s, %s)", (v, sundial.datetime.now())) + cur.close() + self.db.commit() + if args.verbose: + print "Updated", self.name, "to", v + + def close(self): + self.db.close() + +class Upgrade(object): + """ + One upgrade script. Really, just its filename and the Version + object we parse from its filename, we don't need to read the script + itself except when applying it, but we do need to sort all the + available upgrade scripts into version order. + """ + + glob = os.path.join(rpki.autoconf.datarootdir, + "rpki", "upgrade-scripts", + "upgrade-to-*.py") + + @classmethod + def load_all(cls): + for fn in glob.iglob(cls.glob): + yield cls(fn) + + def __init__(self, fn): + head, sep, tail = self.glob.partition("*") + self.fn = fn + self.version = Version(fn[len(head):-len(tail)]) + + def __cmp__(self, other): + return cmp(self.version, other.version) + + def apply(self): + if args.verbose: + print "Applying", fn + with open(self.fn, "r") as f: + exec f + +os.environ["TZ"] = "UTC" +time.tzset() + +parser = argparse.ArgumentParser(description = __doc__) +parser.add_argument("-c", "--config", + help = "override default location of configuration file") +parser.add_argument("-v", "--verbose", + action = "store_true", + help = "natter about whatever we're doing") +args = parser.parse_args() + +cfg = rpki.config.parser(args.config, "myrpki") + +rpkid_db = Database("rpkid") +pubd_db = Database("pubd") + +for upgrade in sorted(Upgrade.load_all()): + if upgrade.version > rpkid_db.version upgrade.version > pubd_db.version: + upgrade.apply() + rpkid_db.version = upgrade.version + pubd_db.version = upgrade.version + +rpkid_db.close() +pubd_db.close() + |