diff options
author | Rob Austein <sra@hactrn.net> | 2014-04-05 22:42:12 +0000 |
---|---|---|
committer | Rob Austein <sra@hactrn.net> | 2014-04-05 22:42:12 +0000 |
commit | fe0bf509f528dbdc50c7182f81057c6a4e15e4bd (patch) | |
tree | 07c9a923d4a0ccdfea11c49cd284f6d5757c5eda /ca/rpki-sql-setup | |
parent | aa28ef54c271fbe4d52860ff8cf13cab19e2207c (diff) |
Source tree reorg, phase 1. Almost everything moved, no file contents changed.
svn path=/branches/tk685/; revision=5757
Diffstat (limited to 'ca/rpki-sql-setup')
-rwxr-xr-x | ca/rpki-sql-setup | 311 |
1 files changed, 311 insertions, 0 deletions
diff --git a/ca/rpki-sql-setup b/ca/rpki-sql-setup new file mode 100755 index 00000000..40a78532 --- /dev/null +++ b/ca/rpki-sql-setup @@ -0,0 +1,311 @@ +#!/usr/bin/env python + +# $Id$ +# +# Copyright (C) 2014 Dragon Research Labs ("DRL") +# Portions copyright (C) 2009-2013 Internet Systems Consortium ("ISC") +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notices and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND DRL AND ISC DISCLAIM ALL +# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL DRL OR +# 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. + +import os +import sys +import glob +import getpass +import argparse +import datetime +import rpki.config +import rpki.version +import rpki.autoconf +import rpki.sql_schemas + +from rpki.mysql_import import MySQLdb, _mysql_exceptions + +ER_NO_SUCH_TABLE = 1146 # See mysqld_ername.h + + +class RootDB(object): + """ + Class to wrap MySQL actions that require root-equivalent access so + we can defer such actions until we're sure they're really needed. + Overall goal here is to prompt the user for the root password once + at most, and not at all when not necessary. + """ + + def __init__(self, mysql_defaults = None): + self.initialized = False + self.mysql_defaults = mysql_defaults + + def __getattr__(self, name): + if self.initialized: + raise AttributeError + if self.mysql_defaults is None: + self.db = MySQLdb.connect(db = "mysql", + user = "root", + passwd = getpass.getpass("Please enter your MySQL root password: ")) + else: + mysql_cfg = rpki.config.parser(self.mysql_defaults, "client") + self.db = MySQLdb.connect(db = "mysql", + user = mysql_cfg.get("user"), + passwd = mysql_cfg.get("password")) + self.cur = self.db.cursor() + self.cur.execute("SHOW DATABASES") + self.databases = set(d[0] for d in self.cur.fetchall()) + self.initialized = True + return getattr(self, name) + + def close(self): + if self.initialized: + self.db.close() + + +class UserDB(object): + """ + Class to wrap MySQL access parameters for a particular database. + + NB: The SQL definitions 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 magic 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.database = cfg.get("sql-database", section = name) + self.username = cfg.get("sql-username", section = name) + self.password = cfg.get("sql-password", section = name) + self.db = None + self.cur = None + + def open(self): + self.db = MySQLdb.connect(db = self.database, user = self.username, passwd = self.password) + self.db.autocommit(False) + self.cur = self.db.cursor() + + def close(self): + if self.cur is not None: + self.cur.close() + self.cur = None + if self.db is not None: + self.db.commit() + self.db.close() + self.db = None + + @property + def exists_and_accessible(self): + try: + MySQLdb.connect(db = self.database, user = self.username, passwd = self.password).close() + except: + return False + else: + return True + + @property + def version(self): + try: + self.cur.execute("SELECT version FROM upgrade_version") + v = self.cur.fetchone() + return Version(None if v is None else v[0]) + except _mysql_exceptions.ProgrammingError, e: + if e.args[0] != ER_NO_SUCH_TABLE: + raise + log("Creating upgrade_version table in %s" % self.name) + self.cur.execute(self.upgrade_version_table_schema) + return Version(None) + + @version.setter + def version(self, v): + if v > self.version: + self.cur.execute("DELETE FROM upgrade_version") + self.cur.execute("INSERT upgrade_version (version, updated) VALUES (%s, %s)", (v, datetime.datetime.now())) + self.db.commit() + log("Updated %s to %s" % (self.name, v)) + + @property + def schema(self): + lines = [] + for line in getattr(rpki.sql_schemas, self.name, "").splitlines(): + line = " ".join(line.split()) + if line and not line.startswith("--"): + lines.append(line) + return [statement.strip() for statement in " ".join(lines).rstrip(";").split(";") if statement.strip()] + + +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) + + +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. + """ + + @classmethod + def load_all(cls, name, dir): + g = os.path.join(dir, "upgrade-%s-to-*.py" % name) + for fn in glob.iglob(g): + yield cls(g, fn) + + def __init__(self, g, fn): + head, sep, tail = g.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, db): + # db is an argument here primarily so the script we exec can get at it + log("Applying %s to %s" % (self.fn, db.name)) + with open(self.fn, "r") as f: + exec f + + +def do_drop(name): + db = UserDB(name) + if db.database in root.databases: + log("DROP DATABASE %s" % db.database) + root.cur.execute("DROP DATABASE %s" % db.database) + root.db.commit() + +def do_create(name): + db = UserDB(name) + log("CREATE DATABASE %s" % db.database) + root.cur.execute("CREATE DATABASE %s" % db.database) + log("GRANT ALL ON %s.* TO %s@localhost IDENTIFIED BY ###" % (db.database, db.username)) + root.cur.execute("GRANT ALL ON %s.* TO %s@localhost IDENTIFIED BY %%s" % (db.database, db.username), + (db.password,)) + root.db.commit() + db.open() + for statement in db.schema: + if not statement.upper().startswith("DROP TABLE"): + log(statement) + db.cur.execute(statement) + db.version = current_version + db.close() + +def do_script_drop(name): + db = UserDB(name) + print "DROP DATABASE IF EXISTS %s;" % db.database + +def do_drop_and_create(name): + do_drop(name) + do_create(name) + +def do_fix_grants(name): + db = UserDB(name) + if not db.exists_and_accessible: + log("GRANT ALL ON %s.* TO %s@localhost IDENTIFIED BY ###" % (db.database, db.username)) + root.cur.execute("GRANT ALL ON %s.* TO %s@localhost IDENTIFIED BY %%s" % (db.database, db.username), + (db.password,)) + root.db.commit() + +def do_create_if_missing(name): + db = UserDB(name) + if not db.exists_and_accessible: + do_create(name) + +def do_apply_upgrades(name): + upgrades = sorted(Upgrade.load_all(name, args.upgrade_scripts)) + if upgrades: + db = UserDB(name) + db.open() + log("Current version of %s is %s" % (db.name, db.version)) + for upgrade in upgrades: + if upgrade.version > db.version: + upgrade.apply(db) + db.version = upgrade.version + db.version = current_version + db.close() + +def log(text): + if args.verbose: + print "#", text + +parser = argparse.ArgumentParser(description = """\ +Automated setup of all SQL stuff used by the RPKI CA tools. Pulls +configuration from rpki.conf, prompts for MySQL password when needed. +""") +group = parser.add_mutually_exclusive_group() +parser.add_argument("-c", "--config", + help = "specify alternate location for rpki.conf") +parser.add_argument("-v", "--verbose", action = "store_true", + help = "whistle while you work") +parser.add_argument("--mysql-defaults", + help = "specify MySQL root access credentials via a configuration file") +parser.add_argument("--upgrade-scripts", + default = os.path.join(rpki.autoconf.datarootdir, "rpki", "upgrade-scripts"), + help = "override default location of upgrade scripts") +group.add_argument("--create", + action = "store_const", dest = "dispatch", const = do_create, + help = "create databases and load schemas") +group.add_argument("--drop", + action = "store_const", dest = "dispatch", const = do_drop, + help = "drop databases") +group.add_argument("--script-drop", + action = "store_const", dest = "dispatch", const = do_script_drop, + help = "send SQL commands to drop databases to standard output") +group.add_argument("--drop-and-create", + action = "store_const", dest = "dispatch", const = do_drop_and_create, + help = "drop databases then recreate them and load schemas") +group.add_argument("--fix-grants", + action = "store_const", dest = "dispatch", const = do_fix_grants, + help = "whack database access to match current configuration file") +group.add_argument("--create-if-missing", + action = "store_const", dest = "dispatch", const = do_create_if_missing, + help = "create databases and load schemas if they don't exist already") +group.add_argument("--apply-upgrades", + action = "store_const", dest = "dispatch", const = do_apply_upgrades, + help = "apply upgrade scripts to existing databases") +parser.set_defaults(dispatch = do_create_if_missing) +args = parser.parse_args() + +try: + cfg = rpki.config.parser(args.config, "myrpki") + root = RootDB(args.mysql_defaults) + current_version = Version(rpki.version.VERSION) + for name in ("irdbd", "rpkid", "pubd"): + if cfg.getboolean("start_" + name, False): + args.dispatch(name) + root.close() +except Exception, e: + #sys.exit(str(e)) + raise |