From def95e42fda9a9e45bdf4f40a6ecb93b644dbf0b Mon Sep 17 00:00:00 2001 From: Rob Austein Date: Mon, 15 Feb 2016 07:51:40 +0000 Subject: Checkpoint while shuffling stuff around for new installation scheme. Installation of everything but Debian packages is probably broken, and Debian packages are too at the moment due to (probably) minor errors in buildtools/debian-skeleton, but enough has changed that this really needs a snapshot checked in for backup. svn path=/branches/tk705/; revision=6257 --- rp/config/Makefile.in | 88 ++++ rp/config/rpki-confgen | 282 +++++++++++ rp/config/rpki-confgen.xml | 1111 ++++++++++++++++++++++++++++++++++++++++++++ rp/config/rpki-manage | 15 + rp/config/rpki-sql-backup | 53 +++ rp/config/rpki-sql-setup | 348 ++++++++++++++ 6 files changed, 1897 insertions(+) create mode 100644 rp/config/Makefile.in create mode 100755 rp/config/rpki-confgen create mode 100644 rp/config/rpki-confgen.xml create mode 100755 rp/config/rpki-manage create mode 100755 rp/config/rpki-sql-backup create mode 100755 rp/config/rpki-sql-setup (limited to 'rp/config') diff --git a/rp/config/Makefile.in b/rp/config/Makefile.in new file mode 100644 index 00000000..40d0a882 --- /dev/null +++ b/rp/config/Makefile.in @@ -0,0 +1,88 @@ +# $Id$ + +PYTHON = @PYTHON@ + +INSTALL = @INSTALL@ -m 555 + +prefix = @prefix@ +exec_prefix = @exec_prefix@ +datarootdir = @datarootdir@ +datadir = @datadir@ +localstatedir = @localstatedir@ +sharedstatedir = @sharedstatedir@ +sysconfdir = @sysconfdir@ +bindir = @bindir@ +sbindir = @sbindir@ +libexecdir = @libexecdir@ +sysconfdir = @sysconfdir@ + +abs_builddir = @abs_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +abs_top_builddir= @abs_top_builddir@ +srcdir = @srcdir@ + +CFG_INSTALL_TARGETS = @CFG_INSTALL_TARGETS@ + +all:: rpki.rp.xml rpki.rp.sample.conf + +clean:: + cd tests; $(MAKE) $@ + +install:: ${CFG_INSTALL_TARGETS} + +install-always:: all + @echo + @echo "== Default configuration file location is ${sysconfdir}/rpki.conf ==" + @echo + ${INSTALL} -d ${DESTDIR}${sysconfdir}/rpki + ${INSTALL} rpki.rp.xml rpki.rp.sample.conf ${DESTDIR}${sysconfdir}/rpki + +test uninstall deinstall:: + @true + +distclean:: clean + rm -f Makefile + +rpki.rp.xml: ${abs_top_srcdir}/rpki/autoconf.py rpki-confgen rpki-confgen.xml + ${PYTHON} rpki-confgen \ + --read-xml rpki-confgen.xml \ + --autoconf \ + --set myrpki::handle=`hostname -f | sed 's/[.]/_/g'` \ + --set myrpki::rpkid_server_host=`hostname -f` \ + --set myrpki::pubd_server_host=`hostname -f` \ + --pwgen myrpki::shared_sql_password \ + --pwgen web_portal::secret-key \ + --set myrpki::run_rpkid=no \ + --set myrpki::run_pubd=no \ + --write-xml $@ + +rpki.rp.sample.conf: rpki.rp.xml + ${PYTHON} rpki-confgen \ + --read-xml rpki.rp.xml \ + --write-conf $@ + +clean:: + rm -f rpki.rp.xml rpki.rp.sample.conf + +install-postconf: \ + install-user install-conf install-sql install-django + +# This should create user "rpki" and group "rpki", but rcynic already +# does that...but we probably need to do it here instead, bother. + +install-user: + @true + +install-conf: + test -f ${DESTDIR}${sysconfdir}/rpki.conf ||\ + cp -p ${DESTDIR}${sysconfdir}/rpki/rpki.rp.sample.conf ${DESTDIR}${sysconfdir}/rpki.conf + +#uninstall deinstall:: +# rm -f ${DESTDIR}${sysconfdir}/rpki/rpki.rp.xml ${DESTDIR}${sysconfdir}/rpki/rpki.rp.sample.conf + +install-sql: + ${sbindir}/rpki-sql-setup create + +install-django: + ${sbindir}/rpki-manage syncdb --noinput + ${sbindir}/rpki-manage migrate app diff --git a/rp/config/rpki-confgen b/rp/config/rpki-confgen new file mode 100755 index 00000000..e6780446 --- /dev/null +++ b/rp/config/rpki-confgen @@ -0,0 +1,282 @@ +#!/usr/bin/env python + +# $Id$ +# +# Copyright (C) 2014 Dragon Research Labs ("DRL") +# Portions copyright (C) 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 argparse +import base64 +import textwrap + +from lxml.etree import Element, SubElement, ElementTree, Comment + +space4 = " " * 4 +space6 = " " * 6 +space8 = " " * 8 +star78 = "*" * 78 + +wiki_wrapper = textwrap.TextWrapper() +conf_wrapper = textwrap.TextWrapper(initial_indent = "# ", subsequent_indent = "# ") +xml6_wrapper = textwrap.TextWrapper(initial_indent = space6, subsequent_indent = space6) +xml8_wrapper = textwrap.TextWrapper(initial_indent = space8, subsequent_indent = space8) + +class Option(object): + + def __init__(self, name, value, doc): + self.name = name + self.value = value + self.doc = doc + + @property + def width(self): + return len(self.name) + + def to_xml(self): + x = Element("option", name = self.name) + if self.value is not None: + x.set("value", self.value) + for d in self.doc: + SubElement(x, "doc").text = "\n" + xml8_wrapper.fill(d) + "\n" + space6 + return x + + def to_wiki(self, f): + f.write("\n== {0.name} == #{0.name}\n".format(self)) + for d in self.doc: + f.write("\n{0}\n".format(wiki_wrapper.fill(d))) + if self.value is None: + f.write("\n{0}\n".format(wiki_wrapper.fill("No default value."))) + else: + f.write("\n{{{{{{\n#!ini\n{0.name} = {0.value}\n}}}}}}\n".format(self)) + + def to_conf(self, f, width): + for i, d in enumerate(self.doc): + f.write("{}\n{}\n".format("" if i == 0 else "#", + conf_wrapper.fill(d))) + if self.value is None: + f.write("\n#{1.name:{0}} = ???\n".format(width - 1, self)) + else: + f.write("\n{1.name:{0}} = {1.value}\n".format(width, self)) + +class Section(object): + + def __init__(self, name): + self.name = name + self.doc = [] + self.options = [] + + @property + def width(self): + return max(o.width for o in self.options) + + @classmethod + def from_xml(cls, elt): + self = cls(name = elt.get("name")) + for x in elt.iterchildren("doc"): + self.doc.append(" ".join(x.text.split())) + for x in elt.iterchildren("option"): + self.options.append(Option(name = x.get("name"), value = x.get("value"), + doc = [" ".join(d.text.split()) + for d in x.iterchildren("doc")])) + return self + + def to_xml(self): + x = Element("section", name = self.name) + for d in self.doc: + SubElement(x, "doc").text = "\n" + xml6_wrapper.fill(d) + "\n" + space4 + x.extend(o.to_xml() for o in self.options) + return x + + def to_wiki(self, f): + f.write("\n= [{0}] section = #{0}\n".format(self.name)) + for d in self.doc: + f.write("\n{0}\n".format(wiki_wrapper.fill(d))) + for o in self.options: + o.to_wiki(f) + + def to_conf(self, f, width): + f.write("\n" + "#" * 78 + "\n\n[" + self.name + "]\n") + if self.doc: + f.write("\n##") + for i, d in enumerate(self.doc): + f.write("{}\n{}\n".format("" if i == 0 else "#", + conf_wrapper.fill(d))) + f.write("##\n") + for o in self.options: + o.to_conf(f, width) + +def wiki_header(f, ident, toc): + f.write(textwrap.dedent('''\ + {{{{{{ + #!comment + + {star78} + THIS PAGE WAS GENERATED AUTOMATICALLY, DO NOT EDIT. + + Generated from {ident} + by $Id$ + {star78} + + }}}}}} + '''.format(star78 = star78, + ident = ident))) + if toc is not None: + f.write("[[TracNav({})]]\n".format(toc)) + f.write("[[PageOutline]]\n") + +def conf_header(f, ident): + f.write(textproc.dedent('''\ + # Automatically generated. Edit as needed, but be careful of overwriting. + # + # Generated from {ident} + # by $Id$ + + '''.format(ident = ident))) + + +# http://stackoverflow.com/questions/9027028/argparse-argument-order + +class CustomAction(argparse.Action): + + def __call__(self, parser, namespace, values, option_string = None): + if not "ordered_args" in namespace: + namespace.ordered_args = [] + namespace.ordered_args.append((self.dest, values)) + +class CustomFlagAction(CustomAction): + + def __init__(self, option_strings, dest, default = None, + required = False, help = None): # pylint: disable=W0622 + super(CustomFlagAction, self).__init__( + option_strings = option_strings, + dest = dest, + nargs = 0, + const = None, + default = default, + required = required, + help = help) + + +class main(object): + + def __init__(self): + self.sections = [] + self.section_map = None + self.option_map = None + self.ident = None + self.toc = None + + parser = argparse.ArgumentParser(description = __doc__) + parser.add_argument("--read-xml", type = argparse.FileType("r"), metavar = "FILE", action = CustomAction, help = "XML input file defining sections and options", required = True) + parser.add_argument("--write-xml", type = argparse.FileType("w"), metavar = "FILE", action = CustomAction, help = "XML output file to snapshot configuration") + parser.add_argument("--write-conf", type = argparse.FileType("w"), metavar = "FILE", action = CustomAction, help = "rpki.conf configuration file to write") + parser.add_argument("--write-wiki", type = argparse.FileType("w"), metavar = "FILE", action = CustomAction, help = "TracWiki file to write (monolithic)") + parser.add_argument("--write-wiki-pages", metavar = "PATTERN", action = CustomAction, help = "TracWiki filenames (pattern) to write (one section per page)") + parser.add_argument("--set", metavar = "VARVAL", action = CustomAction, help = "variable setting in form \"VAR=VAL\"") + parser.add_argument("--pwgen", metavar = "VAR", action = CustomAction, help = "set variable to generated password") + parser.add_argument("--toc", metavar = "TOCVAL", action = CustomAction, help = "set TOC value to use with TracNav plugin") + parser.add_argument("--autoconf", action = CustomFlagAction, help = "configure [autoconf] section") + args = parser.parse_args() + + for cmd, arg in args.ordered_args: + getattr(self, "do_" + cmd)(arg) + + def do_read_xml(self, arg): + self.option_map = None + root = ElementTree(file = arg).getroot() + self.ident = root.get("ident") + self.sections.extend(Section.from_xml(x) for x in root) + self.option_map = {} + self.section_map = {} + for section in self.sections: + if section.name in self.section_map: + sys.exit("Duplicate section {}".format(section.name)) + self.section_map[section.name] = section + for option in section.options: + name = (section.name, option.name) + if name in self.option_map: + sys.exit("Duplicate option {}::{}".format(*name)) + self.option_map[name] = option + + def do_set(self, arg): + try: + name, value = arg.split("=", 1) + section, option = name.split("::") + except ValueError: + sys.exit("Couldn't parse --set specification \"{}\"".format(arg)) + name = (section, option) + if name not in self.option_map: + sys.exit("Couldn't find option {}::{}".format(*name)) + self.option_map[name].value = value + + def do_pwgen(self, arg): + try: + section, option = arg.split("::") + except ValueError: + sys.exit("Couldn't parse --pwgen specification \"{}\"".format(arg)) + name = (section, option) + if name not in self.option_map: + sys.exit("Couldn't find option {}::{}".format(name)) + self.option_map[name].value = base64.urlsafe_b64encode(os.urandom(66)) + + def do_autoconf(self, ignored): + try: + import rpki.autoconf + for option in self.section_map["autoconf"].options: + try: + option.value = getattr(rpki.autoconf, option.name) + except AttributeError: + pass + except ImportError: + sys.exit("rpki.autoconf module is not available") + except KeyError: + sys.exit("Couldn't find autoconf section") + + def do_write_xml(self, arg): + x = Element("configuration", ident = self.ident) + x.append(Comment(" Machine-editable configuration snapshot, generated automatically, do not touch ")) + x.extend(s.to_xml() for s in self.sections) + ElementTree(x).write(arg, pretty_print = True, encoding = "us-ascii") + + def do_write_wiki(self, arg): + for i, section in enumerate(self.sections): + if i == 0: + wiki_header(arg, self.ident, self.toc) + else: + arg.write("\f\n") + section.to_wiki(arg) + + def do_write_wiki_pages(self, arg): + for section in self.sections: + with open(arg % section.name, "w") as f: + wiki_header(f, self.ident, self.toc) + section.to_wiki(f) + + def do_write_conf(self, arg): + with open(arg, "w") as f: + conf_header(f, self.ident) + width = max(s.width for s in self.sections) + for section in self.sections: + section.to_conf(f, width) + + def do_toc(self, arg): + self.toc = arg + + +if __name__ == "__main__": + main() diff --git a/rp/config/rpki-confgen.xml b/rp/config/rpki-confgen.xml new file mode 100644 index 00000000..1b86a140 --- /dev/null +++ b/rp/config/rpki-confgen.xml @@ -0,0 +1,1111 @@ + + + + +
+ + + The "`[myrpki]`" section contains all the parameters that you + really need to configure. The name "`myrpki`" is historical and + may change in the future. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + rcynicng, unlike it's predecessor, uses the same `rpki.conf` + file as all the other programs in the RPKI toolkit. Start + rcynicng with "`-c filename`" to choose a different + configuration file. All options are in the "`[rcynic]`" + section. + + + + This section isn't really fleshed out yet, and just contains the + settings needed for the new SQL code to work. This will change + as the stuff that's currently only configurable on rcynicng's + command line becomes integrated with the configuration file. + + + + + + + + + + +
+ +
+ + + rpkid's default config file is the system `rpki.conf` file. + Start rpkid with "`-c filename`" to choose a different config + file. All options are in the "`[rpkid]`" section. BPKI + Certificates and keys may be in either DER or PEM format. + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + irdbd's default configuration file is the system `rpki.conf` + file. Start irdbd with "`-c filename`" to choose a different + configuration file. All options are in the "`[irdbd]`" section. + + + + Since irdbd is part of the back-end system, it has direct access to + the back-end's SQL database, and thus is able to pull its own BPKI + configuration directly from the database, and thus needs a bit less + configuration than the other daemons. + + + + + + + + + + + + + + + + +
+ +
+ + + pubd's default configuration file is the system `rpki.conf` + file. Start pubd with "`-c filename`" to choose a different + configuration file. All options are in the "`[pubd]`" section. + BPKI certificates and keys may be either DER or PEM format. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + You don't need to run rootd unless you're IANA, are certifying + private address space, or are an RIR which refuses to accept IANA as + the root of the public address hierarchy. + + + + Ok, if that wasn't enough to scare you off: rootd is a mess, + needs to be rewritten, or, better, merged into rpkid, and + requires far too many configuration parameters. + + + + rootd was originally intended to be a very simple program which + simplified rpkid enormously by moving one specific task (acting + as the root CA of an RPKI certificate hierarchy) out of rpkid. + As the specifications and code (mostly the latter) have evolved, + however, this task has become more complicated, and rootd would + have to become much more complicated to keep up. + + + + Don't run rootd unless you're sure that you need to do so. + + + + Still think you need to run rootd? OK, but remember, you have + been warned.... + + + + rootd's default configuration file is the system `rpki.conf` + file. Start rootd with "`-c filename`" to choose a different + configuration file. All options are in the "`[rootd]`" section. + Certificates and keys may be in either DER or PEM format. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + Glue to allow Django to pull user configuration from this file + rather than requiring the user to edit settings.py. + + + + + + + + + + +
+ +
+ + + rpki-confgen --autoconf records the current autoconf settings + here, so that other options can refer to them. The section name + "autoconf" is magic, don't change it. + + + + + + + + + + +
+ +
diff --git a/rp/config/rpki-manage b/rp/config/rpki-manage new file mode 100755 index 00000000..16d0990d --- /dev/null +++ b/rp/config/rpki-manage @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +import os + +# django-admin seems to have problems creating the superuser account when +# $LANG is unset or is set to something totally incompatible with UTF-8. + +if os.environ.get("LANG") in (None, "", "C"): + os.environ["LANG"] = "en_US.UTF-8" + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rpki.django_settings.gui") + +from django.core.management import execute_from_command_line + +execute_from_command_line() diff --git a/rp/config/rpki-sql-backup b/rp/config/rpki-sql-backup new file mode 100755 index 00000000..986e10b1 --- /dev/null +++ b/rp/config/rpki-sql-backup @@ -0,0 +1,53 @@ +#!/usr/bin/env python + +# $Id$ +# +# Copyright (C) 2014 Dragon Research Labs ("DRL") +# Portions copyright (C) 2010-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. + +""" +Back up data from SQL databases, looking at config file to figure out +which databases and what credentials to use with them. +""" + +import subprocess +import os +import argparse +import sys +import time +import rpki.config + +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("-o", "--output", + type = argparse.FileType("wb"), default = sys.stdout, + help = "destination for SQL dump (default: stdout)") +args = parser.parse_args() + +cfg = rpki.config.parser(set_filename = args.config, section = "myrpki") + +for name in ("rpkid", "irdbd", "pubd"): + if cfg.getboolean("start_" + name, False): + subprocess.check_call( + ("mysqldump", "--add-drop-database", + "-u", cfg.get("sql-username", section = name), + "-p" + cfg.get("sql-password", section = name), + "-B", cfg.get("sql-database", section = name)), + stdout = args.output) diff --git a/rp/config/rpki-sql-setup b/rp/config/rpki-sql-setup new file mode 100755 index 00000000..c72c97c5 --- /dev/null +++ b/rp/config/rpki-sql-setup @@ -0,0 +1,348 @@ +#!/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. + +""" +Automated setup of SQL stuff used by the RPKI tools. Pulls +configuration from rpki.conf, prompts for SQL password when needed. +""" + +import os +import pwd +import sys +import getpass +import textwrap +import argparse +import rpki.config + + +class Abstract_Driver(object): + + # Kludge to make classes derived from this into singletons. Net + # of a Million Lies says this is Not Pythonic, but it seems to + # work, so long as one doesn't attempt to subclass the resulting + # driver classes. For our purposes, it will do. + + __instance = None + + def __new__(cls, *args, **kwargs): + if cls.__instance is None: + cls.__instance = object.__new__(cls, *args, **kwargs) + return cls.__instance + + def db_accessible(self, udb): + try: + self._db_accessible_test(udb) + except: + return False + else: + return True + + def db_exists(self, udb): + self.execute("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %s", (udb.database,)) + return bool(self.fetchone()[0]) + + def fetchone(self): + return self._cur.fetchone() + + def fetchall(self): + return self._cur.fetchall() + + def close(self): + self._cur.close() + self._db.close() + + def log(self, msg): + if self.args.verbose: + sys.stderr.write(msg + "\n") + + +class MySQL_Driver(Abstract_Driver): + + _initialized = False + + def __init__(self, args): + try: + self.driver + except AttributeError: + from rpki.mysql_import import MySQLdb + self.driver = MySQLdb + self.args = args + + def _db_accessible_test(self, udb): + self.driver.connect(db = udb.database, user = udb.username, passwd = udb.password).close() + + def execute(*args): + try: + self._cur + except AttributeError: + self.log("MySQL driver initializing root connection") + if self.args.mysql_defaults: + mysql_cfg = rpki.config.parser(set_filename = self.args.mysql_defaults, section = "client") + self._db = self.driver.connect(db = "mysql", + user = mysql_cfg.get("user"), + passwd = mysql_cfg.get("password")) + else: + self._db = self.driver.connect(db = "mysql", + user = "root", + passwd = getpass.getpass("Please enter your MySQL root password: ")) + self._db.autocommit(True) + self._cur = self._db.cursor() + self.log("MySQL driver executing {}".format(", ".join(args))) + return self._cur.execute(*args) + + def create(self, udb): + self.execute("CREATE DATABASE IF NOT EXISTS {0.database}".format(udb)) + self.fix_grants(udb) + + def drop(self, udb): + self.execute("DROP DATABASE IF EXISTS {0.database}".format(udb)) + + def script_drop(self, udb): + self.args.script_output.write("DROP DATABASE IF EXISTS {};\n".format(udb.database)) + + def fix_grants(self, udb): + self.execute("GRANT ALL ON {0.database}.* TO {0.username}@localhost IDENTIFIED BY %s".format(udb), + (udb.password,)) + +class SQLite3_Driver(Abstract_Driver): + + def __init__(self, args): + try: + self.driver + except AttributeError: + import sqlite3 + self.driver = sqlite3 + self.args = args + + def _db_accessible_test(self, udb): + self.driver.connect(udb.database).close() + + def db_exists(self, udb): + return os.path.exists(udb.database) + + def _grant(self, udb): + if udb.username and os.geteuid() == 0: + pw = pwd.getpwnam(udb.username) + os.chown(udb.database, pw.pw_uid, pw.pw_gid) + + def create(self, udb): + self._db_accessible_test(udb.database) + self._grant(udb) + + def drop(self, udb): + os.unlink(udb.database) + + def script_drop(self, udb): + self.args.script_output.write("rm {}\n".format(udb.database)) + + def fix_grants(self, udb): + self._grant(udb) + + +class PostgreSQL_Driver(Abstract_Driver): + + def __init__(self, args): + try: + self.driver + except AttributeError: + import psycopg2 + self.driver = psycopg2 + self.args = args + if args.postgresql_root_username and (os.getuid() == 0 or os.geteuid() == 0): + self._pw = pwd.getpwnam(args.postgresql_root_username) + else: + self._pw = None + self.log("Initialized PostgreSQL driver, pw {!r}".format(self._pw)) + + def _seteuid(self, new_uid): + old_uid = os.geteuid() + if new_uid != old_uid: + self.log("PostgreSQL driver changing EUID from {} to {}".format(old_uid, new_uid)) + os.seteuid(new_uid) + return old_uid + + def execute(self, *args): + try: + self._cur + except AttributeError: + self.log("PostgreSQL driver opening connection to database {}".format(self.args.postgresql_root_database)) + if self._pw is not None: + euid = self._seteuid(self._pw.pw_uid) + try: + self._db = self.driver.connect(database = self.args.postgresql_root_database) + self._db.autocommit = True + self._cur = self._db.cursor() + finally: + if self._pw is not None: + self._seteuid(euid) + self.log("PostgreSQL driver executing {}".format(", ".join(args))) + return self._cur.execute(*args) + + def _db_accessible_test(self, udb): + pw = pwd.getpwnam(udb.username) + uid = self._seteuid(pw.pw_uid) + try: + self.driver.connect(database = udb.database, user = udb.username , password = usb.password).close() + finally: + self._seteuid(uid) + + def _role_in_use(self, udb): + self.execute('''\ + SELECT COUNT(*) FROM pg_database + JOIN pg_roles ON pg_database.datdba = pg_roles.oid + WHERE pg_roles.rolname = %s''', (udb.username,)) + return bool(self.fetchone()[0]) + + def create(self, udb): + self.execute('''\ + DO $$ BEGIN + IF NOT EXISTS (SELECT * FROM pg_catalog.pg_roles + WHERE rolname = '{0.username}') + THEN + CREATE ROLE {0.username} LOGIN PASSWORD '{0.password}'; + END IF; + END $$'''.format(udb)) + self.execute("CREATE DATABASE {0.database} OWNER {0.username}".format(udb)) + + def drop(self, udb): + self.execute("DROP DATABASE IF EXISTS {0.database}".format(udb)) + if not self._role_in_use(udb): + self.execute("DROP ROLE IF EXISTS {0.username}".format(udb)) + + def script_drop(self, udb): + self.args.script_output.write(textwrap.dedent('''\ + DROP DATABASE IF EXISTS {0.database}; + DO $$ BEGIN + IF NOT EXISTS (SELECT * FROM pg_database JOIN pg_roles + ON pg_database.datdba = pg_roles.oid + WHERE pg_roles.rolname = '{0.username}') + THEN + DROP ROLE IF EXISTS {0.username}; + END IF; + END $$; + '''.format(udb))) + + def fix_grants(self, udb): + self.execute("ALTER DATABASE {0.database} OWNER TO {0.username}".format(udb)) + self.execute("ALTER ROLE {0.username} WITH PASSWORD '{0.password}".format(udb)) + + +class UserDB(object): + """ + Class to wrap access parameters for a particular database. + """ + + drivers = dict(sqlite3 = SQLite3_Driver, + mysql = MySQL_Driver, + postgresql = PostgreSQL_Driver) + + def __init__(self, args, 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.engine = cfg.get("sql-engine", section = name) + self.driver = self.drivers[self.engine](args) + self.args = args + + def drop(self): + if self.args.force or self.driver.db_accessible(self): + self.driver.drop(self) + + def create(self): + if self.args.force or not self.driver.db_accessible(self): + self.driver.create(self) + + def script_drop(self): + self.driver.script_drop(self) + + def drop_and_create(self): + if self.args.force or self.driver.db_accessible(self): + self.driver.drop(self) + self.driver.create(self) + + def fix_grants(self): + if self.args.force or not self.driver.db_accessible(self): + self.driver.fix_grants(self) + + +parser = argparse.ArgumentParser(description = __doc__) +parser.add_argument("-c", "--config", + help = "specify alternate location for rpki.conf") +parser.add_argument("-d", "--debug", action = "store_true", + help = "enable debugging (eg, Python backtraces)") +parser.add_argument("-v", "--verbose", action = "store_true", + help = "whistle while you work") +parser.add_argument("-f", "--force", action = "store_true", + help = "force database create, drop, or grant regardless of current state") + +parser.add_argument("--mysql-defaults", + help = "specify MySQL root access credentials via a configuration file") + + +parser.add_argument("--postgresql-root-database", default = "postgres", + help = "name of PostgreSQL control database") +parser.add_argument("--postgresql-root-username", + help = "username of PostgreSQL control role") + +subparsers = parser.add_subparsers(title = "Commands", metavar = "", dest = "dispatch") + +subparsers.add_parser("create", + help = "create databases and load schemas") + +subparsers.add_parser("drop", + help = "drop databases") + +subparser = subparsers.add_parser("script-drop", + help = "show SQL commands to drop databases") +subparser.add_argument("script_output", + nargs = "?", type = argparse.FileType("w"), default = "-", + help = "destination for drop script") + +subparsers.add_parser("drop-and-create", + help = "drop databases then recreate them and load schemas") + +subparsers.add_parser("fix-grants", + help = "whack database to match configuration file") + +args = parser.parse_args() + +try: + + cfg = rpki.config.parser(set_filename = args.config, section = "myrpki") + + names = [name for name in ("irdbd", "rpkid", "pubd") + if cfg.getboolean("start_" + name, False)] + names.append("rcynic") + + # For now, we quietly ignore missing sections rather than throwing an exception. + # I could make a case either way for this, but ignoring missing sections is a + # lot easier to clean up while debugging the installation scripts. + + for name in names: + if cfg.has_section(name): + udb = UserDB(args = args, name = name) + method = args.dispatch.replace("-", "_") + getattr(udb, method)() + +except Exception, e: + if args.debug: + raise + else: + sys.exit(str(e)) -- cgit v1.2.3