aboutsummaryrefslogtreecommitdiff
path: root/rpkid/testbed.py
diff options
context:
space:
mode:
Diffstat (limited to 'rpkid/testbed.py')
-rw-r--r--rpkid/testbed.py941
1 files changed, 941 insertions, 0 deletions
diff --git a/rpkid/testbed.py b/rpkid/testbed.py
new file mode 100644
index 00000000..97a66a2b
--- /dev/null
+++ b/rpkid/testbed.py
@@ -0,0 +1,941 @@
+# $Id$
+
+# Copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
+#
+# 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 ARIN DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ARIN 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.
+
+"""
+Test framework to configure and drive a collection of rpkid.py and
+irdbd.py instances under control of a master script.
+
+Usage: python rpkid.py [ { -c | --config } config_file ]
+ [ { -h | --help } ]
+ [ { -y | --yaml } yaml_script ]
+
+Default config_file is testbed.conf, override with --config option.
+
+Default yaml_script is testbed.yaml, override with -yaml option.
+
+yaml_script is a YAML file describing the tests to be run, and is
+intended to be implementation agnostic.
+
+config_file contains settings for various implementation-specific
+things that don't belong in yaml_script.
+"""
+
+import os, yaml, MySQLdb, subprocess, signal, time, datetime, re, getopt, sys, lxml
+import rpki.resource_set, rpki.sundial, rpki.x509, rpki.https, rpki.log, rpki.left_right, rpki.config
+
+os.environ["TZ"] = "UTC"
+time.tzset()
+
+cfg_file = "testbed.conf"
+
+yaml_script = None
+
+opts,argv = getopt.getopt(sys.argv[1:], "c:hy:?", ["config=", "help", "yaml="])
+for o,a in opts:
+ if o in ("-h", "--help", "-?"):
+ print __doc__
+ sys.exit(0)
+ elif o in ("-c", "--config"):
+ cfg_file = a
+ elif o in ("-y", "--yaml"):
+ yaml_script = a
+if argv:
+ print __doc__
+ raise RuntimeError, "Unexpected arguments %s" % argv
+
+cfg = rpki.config.parser(cfg_file, "testbed")
+
+# Load the YAML script early, so we can report errors ASAP
+
+if yaml_script is None:
+ yaml_script = cfg.get("yaml_script", "testbed.yaml")
+try:
+ yaml_script = [y for y in yaml.safe_load_all(open(yaml_script))]
+except:
+ print __doc__
+ raise
+
+# Define port allocator early, so we can use it while reading config
+
+def allocate_port():
+ """Allocate a TCP port number."""
+ global base_port
+ p = base_port
+ base_port += 1
+ return p
+
+# Most filenames in the following are relative to the working directory.
+
+testbed_name = cfg.get("testbed_name", "testbed")
+testbed_dir = cfg.get("testbed_dir", testbed_name + ".dir")
+
+irdb_db_pass = cfg.get("irdb_db_pass", "fnord")
+rpki_db_pass = cfg.get("rpki_db_pass", "fnord")
+
+base_port = int(cfg.get("base_port", "4400"))
+
+rsyncd_port = allocate_port()
+rootd_port = allocate_port()
+
+rsyncd_module = cfg.get("rsyncd_module", testbed_name)
+rootd_sia = cfg.get("rootd_sia", "rsync://localhost:%d/%s/" % (rsyncd_port, rsyncd_module))
+
+rootd_name = cfg.get("rootd_name", "rootd")
+rsyncd_name = cfg.get("rcynic_name", "rsyncd")
+rcynic_name = cfg.get("rcynic_name", "rcynic")
+
+prog_python = cfg.get("prog_python", "python")
+prog_rpkid = cfg.get("prog_rpkid", "../rpkid.py")
+prog_irdbd = cfg.get("prog_irdbd", "../irdbd.py")
+prog_poke = cfg.get("prog_poke", "../testpoke.py")
+prog_rootd = cfg.get("prog_rootd", "../rootd.py")
+prog_openssl = cfg.get("prog_openssl", "../../openssl/openssl/apps/openssl")
+prog_rsyncd = cfg.get("prog_rsyncd", "rsync")
+prog_rcynic = cfg.get("prog_rcynic", "../../rcynic/rcynic")
+
+rcynic_stats = cfg.get("rcynic_stats", "xsltproc --param refresh 0 ../../rcynic/rcynic.xsl %s.xml | w3m -T text/html -dump" % rcynic_name)
+
+rpki_sql_file = cfg.get("rpki_sql_file", "../docs/rpki-db-schema.sql")
+irdb_sql_file = cfg.get("irdb_sql_file", "../docs/sample-irdb.sql")
+
+rpki_sql = open(rpki_sql_file).read()
+irdb_sql = open(irdb_sql_file).read()
+
+testbed_key = None
+testbed_certs = None
+rootd_ta = None
+
+
+def main():
+ """Main program, up front to make control logic more obvious."""
+
+ rpki.log.init(testbed_name)
+
+ signal.signal(signal.SIGALRM, wakeup)
+
+ rootd_process = None
+ rsyncd_process = None
+
+ try:
+ os.chdir(testbed_dir)
+ except:
+ os.makedirs(testbed_dir)
+ os.chdir(testbed_dir)
+
+ # Clean up old state
+
+ subprocess.check_call(("rm", "-rf", "publication", "rcynic-data", "rootd.subject.pkcs10", "rootd.req"))
+
+ # Read the first YAML document as our master configuration
+
+ db = allocation_db(yaml_script.pop(0))
+
+ # Construct biz keys and certs for this script to use
+
+ setup_biz_cert_chain(testbed_name)
+ global testbed_key, testbed_certs
+ testbed_key = rpki.x509.RSA(PEM_file = testbed_name + "-EE.key")
+ testbed_certs = rpki.x509.X509_chain(PEM_files = (testbed_name + "-EE.cer", testbed_name + "-CA.cer"))
+
+ # Construct biz keys and certs for rootd instance to use
+
+ setup_biz_cert_chain(rootd_name)
+ global rootd_ta
+ rootd_ta = rpki.x509.X509(PEM_file = rootd_name + "-TA.cer")
+
+ # Construct biz keys and certs for rpkid and irdbd instances.
+
+ for a in db:
+ a.setup_biz_certs()
+
+ # Create the (psuedo) publication directory
+
+ setup_publication()
+
+ # Construct config files for rootd, rsyncd, rcynic instances
+
+ setup_rootd(db.root.name)
+ setup_rsyncd()
+ setup_rcynic()
+
+ # Construct config files for rpkid and irdbd instances
+
+ for a in db.engines:
+ a.setup_conf_file()
+
+ # Initialize SQL for rpkid and irdbd instances
+
+ for a in db.engines:
+ a.setup_sql(rpki_sql, irdb_sql)
+
+ # Populate IRDB(s)
+
+ for a in db.engines:
+ a.sync_sql()
+
+ try:
+
+ # Start rootd instance
+
+ rpki.log.info("Running rootd")
+ rootd_process = subprocess.Popen((prog_python, prog_rootd, "-c", rootd_name + ".conf"))
+
+ # Start rsyncd instance
+
+ rpki.log.info("Running rsyncd")
+ rsyncd_process = subprocess.Popen((prog_rsyncd, "--daemon", "--no-detach", "--config", rsyncd_name + ".conf"))
+
+ # Start rpkid and irdbd instances
+
+ for a in db.engines:
+ a.run_daemons()
+
+ # Wait a little while for all those instances to come up
+
+ rpki.log.info("Sleeping while daemons start up")
+ time.sleep(10)
+
+ # Create objects in RPKI engines
+
+ for a in db.engines:
+ a.create_rpki_objects()
+
+ # Write YAML files for leaves
+
+ for a in db.leaves:
+ a.write_leaf_yaml()
+
+ # 8: Start cycle:
+
+ while True:
+
+ # Run cron in all RPKI instances
+
+ for a in db.engines:
+ a.run_cron()
+
+ # Run all YAML clients
+
+ for a in db.leaves:
+ a.run_yaml()
+
+ # Make sure that everybody got what they were supposed to get
+ # and that everything that was supposed to be published has been
+ # published.
+ #
+ # As a first cut at this, try running rcynic on the outputs.
+
+ run_rcynic()
+
+ # If we've run out of deltas to apply, we're done
+
+ if not yaml_script:
+ break
+
+ # Apply next deltas and resync IRDBs
+
+ db.apply_delta(yaml_script.pop(0))
+
+ for a in db.engines:
+ a.sync_sql()
+
+ # Clean up
+
+ finally:
+
+ try:
+ for a in db.engines:
+ a.kill_daemons()
+ for p,n in ((rootd_process, "rootd"), (rsyncd_process, "rsyncd")):
+ if p is not None:
+ rpki.log.info("Killing %s" % n)
+ os.kill(p.pid, signal.SIGTERM)
+ except Exception, data:
+ rpki.log.warn("Couldn't clean up daemons (%s), continuing" % data)
+
+# Define time delta parser early, so we can use it while reading config
+
+class timedelta(datetime.timedelta):
+ """Timedelta with text parsing. This accepts two input formats:
+
+ - A simple integer, indicating a number of seconds.
+
+ - A string of the form "wD xH yM zS" where w, x, y, and z are integers
+ and D, H, M, and S indicate days, hours, minutes, and seconds.
+ All of the fields are optional, but at least one must be specified.
+ Eg, "3D4H" means "three days plus four hours".
+ """
+
+ ## @var regexp
+ # Hideously ugly regular expression to parse the complex text form.
+ # Tags are intended for use with re.MatchObject.groupdict() and map
+ # directly to the keywords expected by the timedelta constructor.
+
+ regexp = re.compile("\\s*(?:(?P<days>\\d+)D)?" +
+ "\\s*(?:(?P<hours>\\d+)H)?" +
+ "\\s*(?:(?P<minutes>\\d+)M)?" +
+ "\\s*(?:(?P<seconds>\\d+)S)?\\s*", re.I)
+
+ @classmethod
+ def parse(cls, arg):
+ """Parse text into a timedelta object."""
+ if not isinstance(arg, str):
+ return cls(seconds = arg)
+ elif arg.isdigit():
+ return cls(seconds = int(arg))
+ else:
+ return cls(**dict((k, int(v)) for (k, v) in cls.regexp.match(arg).groupdict().items() if v is not None))
+
+ def convert_to_seconds(self):
+ """Convert a timedelta interval to seconds."""
+ return self.days * 24 * 60 * 60 + self.seconds
+
+def wakeup(signum, frame):
+ """Handler called when we receive a SIGALRM signal."""
+ rpki.log.info("Wakeup call received, continuing")
+
+def cmd_sleep(interval = None):
+ """Set an alarm, then wait for it to go off."""
+ if interval is None:
+ rpki.log.info("Pausing indefinitely, send a SIGALRM to wake me up")
+ else:
+ seconds = timedelta.parse(interval).convert_to_seconds()
+ rpki.log.info("Sleeping %s seconds" % seconds)
+ signal.alarm(seconds)
+ signal.pause()
+
+def cmd_shell(*cmd):
+ """Run a shell command."""
+ cmd = " ".join(cmd)
+ status = subprocess.call(cmd, shell = True)
+ rpki.log.info("Shell command returned status %d" % status)
+
+def cmd_echo(*words):
+ """Echo some text to the log."""
+ rpki.log.note(" ".join(words))
+
+## @var cmds
+# Dispatch table for commands embedded in delta sections
+
+cmds = { "sleep" : cmd_sleep,
+ "shell" : cmd_shell,
+ "echo" : cmd_echo }
+
+class allocation_db(list):
+ """Representation of all the entities and allocations in the test system.
+ Almost everything is generated out of this database.
+ """
+
+ def __init__(self, yaml):
+ """Initialize database from the (first) YAML document."""
+ self.root = allocation(yaml, self)
+ assert self.root.is_root()
+ if self.root.crl_interval is None:
+ self.root.crl_interval = timedelta.parse(cfg.get("crl_interval", "1d")).convert_to_seconds()
+ for a in self:
+ if a.sia_base is None and a.parent is not None:
+ a.sia_base = a.parent.sia_base + a.name + "/"
+ elif a.sia_base is None and a.parent is None:
+ a.sia_base = rootd_sia + a.name + "/"
+ if a.base.valid_until is None:
+ a.base.valid_until = a.parent.base.valid_until
+ if a.crl_interval is None:
+ a.crl_interval = a.parent.crl_interval
+ self.root.closure()
+ self.map = dict((a.name, a) for a in self)
+ self.engines = [a for a in self if not a.is_leaf()]
+ self.leaves = [a for a in self if a.is_leaf()]
+ for i, a in zip(range(len(self.engines)), self.engines):
+ a.set_engine_number(i)
+
+ def apply_delta(self, delta):
+ """Apply a delta or run a command."""
+ for d in delta:
+ if isinstance(d, str):
+ c = d.split()
+ cmds[c[0]](*c[1:])
+ else:
+ self.map[d["name"]].apply_delta(d)
+ self.root.closure()
+
+ def dump(self):
+ """Print content of the database."""
+ for a in self:
+ print a
+
+class allocation(object):
+
+ parent = None
+ irdb_db_name = None
+ irdb_port = None
+ rpki_db_name = None
+ rpki_port = None
+ crl_interval = None
+
+ def __init__(self, yaml, db, parent = None):
+ """Initialize one entity and insert it into the database."""
+ db.append(self)
+ self.name = yaml["name"]
+ self.parent = parent
+ self.kids = [allocation(k, db, self) for k in yaml.get("kids", ())]
+ valid_until = yaml.get("valid_until")
+ if valid_until is None and "valid_for" in yaml:
+ valid_until = datetime.datetime.utcnow() + timedelta.parse(yaml["valid_for"])
+ self.base = rpki.resource_set.resource_bag(
+ as = rpki.resource_set.resource_set_as(yaml.get("asn")),
+ v4 = rpki.resource_set.resource_set_ipv4(yaml.get("ipv4")),
+ v6 = rpki.resource_set.resource_set_ipv6(yaml.get("ipv6")),
+ valid_until = valid_until)
+ self.sia_base = yaml.get("sia_base")
+ if "crl_interval" in yaml:
+ self.crl_interval = timedelta.parse(yaml["crl_interval"]).convert_to_seconds()
+ self.extra_conf = yaml.get("extra_conf", [])
+
+ def closure(self):
+ """Compute the transitive resource closure."""
+ resources = self.base
+ for kid in self.kids:
+ resources = resources.union(kid.closure())
+ self.resources = resources
+ return resources
+
+ def apply_delta(self, yaml):
+ """Apply deltas to this entity."""
+ rpki.log.info("Applying delta: %s" % yaml)
+ for k,v in yaml.items():
+ if k != "name":
+ getattr(self, "apply_" + k)(v)
+
+ def apply_add_as(self, text): self.base.as = self.base.as.union(rpki.resource_set.resource_set_as(text))
+ def apply_add_v4(self, text): self.base.v4 = self.base.v4.union(rpki.resource_set.resource_set_ipv4(text))
+ def apply_add_v6(self, text): self.base.v6 = self.base.v6.union(rpki.resource_set.resource_set_ipv6(text))
+ def apply_sub_as(self, text): self.base.as = self.base.as.difference(rpki.resource_set.resource_set_as(text))
+ def apply_sub_v4(self, text): self.base.v4 = self.base.v4.difference(rpki.resource_set.resource_set_ipv4(text))
+ def apply_sub_v6(self, text): self.base.v6 = self.base.v6.difference(rpki.resource_set.resource_set_ipv6(text))
+
+ def apply_valid_until(self, stamp): self.base.valid_until = stamp
+ def apply_valid_for(self, text): self.base.valid_until = datetime.datetime.utcnow() + timedelta.parse(text)
+ def apply_valid_add(self, text): self.base.valid_until += timedelta.parse(text)
+ def apply_valid_sub(self, text): self.base.valid_until -= timedelta.parse(text)
+
+ def apply_rekey(self, target):
+ if self.is_leaf():
+ raise RuntimeError, "Can't rekey YAML leaf %s, sorry" % self.name
+ elif target is None:
+ rpki.log.info("Rekeying <self/> %s" % self.name)
+ self.call_rpkid(rpki.left_right.self_elt.make_pdu(action = "set", self_id = self.self_id, rekey = "yes"))
+ else:
+ rpki.log.info("Rekeying <parent/> %s %s" % (self.name, target))
+ self.call_rpkid(rpki.left_right.parent_elt.make_pdu(action = "set", self_id = self.self_id, parent_id = target, rekey = "yes"))
+
+ def apply_revoke(self, target):
+ if self.is_leaf():
+ rpki.log.info("Attempting to revoke YAML leaf %s" % self.name)
+ subprocess.check_call((prog_python, prog_poke, "-y", self.name + ".yaml", "-r", "revoke"))
+ elif target is None:
+ rpki.log.info("Revoking <self/> %s" % self.name)
+ self.call_rpkid(rpki.left_right.self_elt.make_pdu(action = "set", self_id = self.self_id, revoke = "yes"))
+ else:
+ rpki.log.info("Revoking <parent/> %s %s" % (self.name, target))
+ self.call_rpkid(rpki.left_right.parent_elt.make_pdu(action = "set", self_id = self.self_id, parent_id = target, revoke = "yes"))
+
+ def __str__(self):
+ s = self.name + "\n"
+ if self.resources.as: s += " ASN: %s\n" % self.resources.as
+ if self.resources.v4: s += " IPv4: %s\n" % self.resources.v4
+ if self.resources.v6: s += " IPv6: %s\n" % self.resources.v6
+ if self.kids: s += " Kids: %s\n" % ", ".join(k.name for k in self.kids)
+ if self.parent: s += " Up: %s\n" % self.parent.name
+ if self.sia_base: s += " SIA: %s\n" % self.sia_base
+ return s + "Until: %s\n" % self.resources.valid_until.strftime("%Y-%m-%dT%H:%M:%SZ")
+
+ def is_leaf(self): return not self.kids
+ def is_root(self): return self.parent is None
+ def is_twig(self): return self.parent is not None and self.kids
+
+ def set_engine_number(self, n):
+ """Set the engine number for this entity."""
+ self.irdb_db_name = "irdb%d" % n
+ self.irdb_port = allocate_port()
+ self.rpki_db_name = "rpki%d" % n
+ self.rpki_port = allocate_port()
+
+ def setup_biz_certs(self):
+ """Create business certs for this entity."""
+ rpki.log.info("Biz certs for %s" % self.name)
+ for tag in ("RPKI", "IRDB"):
+ setup_biz_cert_chain(self.name + "-" + tag)
+ self.rpkid_ta = rpki.x509.X509(PEM_file = self.name + "-RPKI-TA.cer")
+
+ def setup_conf_file(self):
+ """Write config files for this entity."""
+ rpki.log.info("Config files for %s" % self.name)
+ d = { "my_name" : self.name,
+ "testbed_name" : testbed_name,
+ "irdb_db_name" : self.irdb_db_name,
+ "irdb_db_pass" : irdb_db_pass,
+ "irdb_port" : self.irdb_port,
+ "rpki_db_name" : self.rpki_db_name,
+ "rpki_db_pass" : rpki_db_pass,
+ "rpki_port" : self.rpki_port }
+ f = open(self.name + ".conf", "w")
+ f.write(conf_fmt_1 % d)
+ for line in self.extra_conf:
+ f.write(line + "\n")
+ f.close()
+
+ def setup_sql(self, rpki_sql, irdb_sql):
+ """Set up this entity's IRDB."""
+ rpki.log.info("MySQL setup for %s" % self.name)
+ db = MySQLdb.connect(user = "rpki", db = self.rpki_db_name, passwd = rpki_db_pass)
+ cur = db.cursor()
+ for sql in rpki_sql.split(";"):
+ cur.execute(sql)
+ db.close()
+ db = MySQLdb.connect(user = "irdb", db = self.irdb_db_name, passwd = irdb_db_pass)
+ cur = db.cursor()
+ for sql in irdb_sql.split(";"):
+ cur.execute(sql)
+ for kid in self.kids:
+ cur.execute("INSERT registrant (IRBE_mapped_id, subject_name, valid_until) VALUES (%s, %s, %s)", (kid.name, kid.name, kid.resources.valid_until))
+ db.close()
+
+ def sync_sql(self):
+ """Whack this entity's IRDB to match our master database. We do
+ this once during setup, then do it again every time we apply a
+ delta to this entity.
+ """
+ rpki.log.info("MySQL sync for %s" % self.name)
+ db = MySQLdb.connect(user = "irdb", db = self.irdb_db_name, passwd = irdb_db_pass)
+ cur = db.cursor()
+ cur.execute("DELETE FROM asn")
+ cur.execute("DELETE FROM net")
+ for kid in self.kids:
+ cur.execute("SELECT registrant_id FROM registrant WHERE IRBE_mapped_id = %s", (kid.name,))
+ registrant_id = cur.fetchone()[0]
+ for as_range in kid.resources.as:
+ cur.execute("INSERT asn (start_as, end_as, registrant_id) VALUES (%s, %s, %s)", (as_range.min, as_range.max, registrant_id))
+ for v4_range in kid.resources.v4:
+ cur.execute("INSERT net (start_ip, end_ip, version, registrant_id) VALUES (%s, %s, 4, %s)", (v4_range.min, v4_range.max, registrant_id))
+ for v6_range in kid.resources.v6:
+ cur.execute("INSERT net (start_ip, end_ip, version, registrant_id) VALUES (%s, %s, 6, %s)", (v6_range.min, v6_range.max, registrant_id))
+ cur.execute("UPDATE registrant SET valid_until = %s WHERE registrant_id = %s", (kid.resources.valid_until, registrant_id))
+ db.close()
+
+ def run_daemons(self):
+ """Run daemons for this entity."""
+ rpki.log.info("Running daemons for %s" % self.name)
+ self.rpkid_process = subprocess.Popen((prog_python, prog_rpkid, "-c", self.name + ".conf"))
+ self.irdbd_process = subprocess.Popen((prog_python, prog_irdbd, "-c", self.name + ".conf"))
+
+ def kill_daemons(self):
+ """Kill daemons for this entity."""
+ rpki.log.info("Killing daemons for %s" % self.name)
+ for proc in (self.rpkid_process, self.irdbd_process):
+ try:
+ os.kill(proc.pid, signal.SIGTERM)
+ except:
+ pass
+ proc.wait()
+
+ def call_rpkid(self, pdu):
+ """Send a left-right message to this entity's RPKI daemon and
+ return the response.
+ """
+ rpki.log.info("Calling rpkid for %s" % self.name)
+ pdu.type = "query"
+ elt = rpki.left_right.msg((pdu,)).toXML()
+ rpki.relaxng.left_right.assertValid(elt)
+ rpki.log.debug(lxml.etree.tostring(elt, pretty_print = True, encoding = "us-ascii"))
+ cms = rpki.cms.xml_sign(
+ elt = elt,
+ key = testbed_key,
+ certs = testbed_certs)
+ url = "https://localhost:%d/left-right" % self.rpki_port
+ rpki.log.debug("Attempting to connect to %s" % url)
+ cms = rpki.https.client(
+ privateKey = testbed_key,
+ certChain = testbed_certs,
+ x509TrustList = rpki.x509.X509_chain(self.rpkid_ta),
+ url = url,
+ msg = cms)
+ elt = rpki.cms.xml_verify(cms = cms, ta = self.rpkid_ta)
+ rpki.relaxng.left_right.assertValid(elt)
+ rpki.log.debug(lxml.etree.tostring(elt, pretty_print = True, encoding = "us-ascii"))
+ pdu = rpki.left_right.sax_handler.saxify(elt)[0]
+ assert pdu.type == "reply" and not isinstance(pdu, rpki.left_right.report_error_elt)
+ return pdu
+
+ def create_rpki_objects(self):
+ """Create RPKI engine objects for this engine.
+
+ Parent and child objects are tricky:
+
+ - Parent object needs to know child_id by which parent refers to
+ this engine in order to set the contact URI correctly.
+
+ - Child object needs to record the child_id by which this engine
+ refers to the child.
+
+ This all just works so long as we walk the set of engines in the
+ right order (parents before their children).
+
+ Root node of the engine tree is special, it too has a parent but
+ that one is the magic self-signed micro engine.
+ """
+
+ rpki.log.info("Creating rpkid self object for %s" % self.name)
+ self.self_id = self.call_rpkid(rpki.left_right.self_elt.make_pdu(action = "create", crl_interval = self.crl_interval)).self_id
+
+ rpki.log.info("Creating rpkid BSC object for %s" % self.name)
+ pdu = self.call_rpkid(rpki.left_right.bsc_elt.make_pdu(action = "create", self_id = self.self_id, generate_keypair = True))
+ self.bsc_id = pdu.bsc_id
+
+ rpki.log.info("Issuing BSC EE cert for %s" % self.name)
+ cmd = (prog_openssl, "x509", "-req", "-CA", self.name + "-RPKI-CA.cer", "-CAkey", self.name + "-RPKI-CA.key", "-CAserial", self.name + "-RPKI-CA.srl")
+ signer = subprocess.Popen(cmd, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
+ bsc_ee = rpki.x509.X509(PEM = signer.communicate(input = pdu.pkcs10_cert_request.get_PEM())[0])
+
+ rpki.log.info("Installing BSC EE cert for %s" % self.name)
+ self.call_rpkid(rpki.left_right.bsc_elt.make_pdu(action = "set", self_id = self.self_id, bsc_id = self.bsc_id,
+ signing_cert = [bsc_ee, rpki.x509.X509(PEM_file = self.name + "-RPKI-CA.cer")]))
+
+ rpki.log.info("Creating rpkid repository object for %s" % self.name)
+ self.repository_id = self.call_rpkid(rpki.left_right.repository_elt.make_pdu(action = "create", self_id = self.self_id, bsc_id = self.bsc_id)).repository_id
+
+ rpki.log.info("Creating rpkid parent object for %s" % self.name)
+ if self.parent is None:
+ self.parent_id = self.call_rpkid(rpki.left_right.parent_elt.make_pdu(
+ action = "create", self_id = self.self_id, bsc_id = self.bsc_id, repository_id = self.repository_id, sia_base = self.sia_base,
+ cms_ta = rootd_ta, https_ta = rootd_ta, sender_name = self.name, recipient_name = "Walrus",
+ peer_contact_uri = "https://localhost:%s/" % rootd_port)).parent_id
+ else:
+ self.parent_id = self.call_rpkid(rpki.left_right.parent_elt.make_pdu(
+ action = "create", self_id = self.self_id, bsc_id = self.bsc_id, repository_id = self.repository_id, sia_base = self.sia_base,
+ cms_ta = self.parent.rpkid_ta, https_ta = self.parent.rpkid_ta, sender_name = self.name, recipient_name = self.parent.name,
+ peer_contact_uri = "https://localhost:%s/up-down/%s" % (self.parent.rpki_port, self.child_id))).parent_id
+
+ rpki.log.info("Creating rpkid child objects for %s" % self.name)
+ db = MySQLdb.connect(user = "irdb", db = self.irdb_db_name, passwd = irdb_db_pass)
+ cur = db.cursor()
+ for kid in self.kids:
+ kid.child_id = self.call_rpkid(rpki.left_right.child_elt.make_pdu(action = "create", self_id = self.self_id, bsc_id = self.bsc_id, cms_ta = kid.rpkid_ta)).child_id
+ cur.execute("UPDATE registrant SET rpki_self_id = %s, rpki_child_id = %s WHERE IRBE_mapped_id = %s", (self.self_id, kid.child_id, kid.name))
+ db.close()
+
+ def write_leaf_yaml(self):
+ """Write YAML scripts for leaf nodes. Only supports list requests
+ at the moment: issue requests would require class and SIA values,
+ revoke requests would require class and SKI values.
+
+ ...Except that we can cheat and assume class 1 because we just
+ know that rpkid will assign that with the current setup. So we
+ also support issue, kludge though this is.
+ """
+
+ rpki.log.info("Writing leaf YAML for %s" % self.name)
+ f = open(self.name + ".yaml", "w")
+ f.write(yaml_fmt_1 % {
+ "child_id" : self.child_id,
+ "parent_name" : self.parent.name,
+ "my_name" : self.name,
+ "https_port" : self.parent.rpki_port,
+ "sia" : self.sia_base })
+ f.close()
+
+ def run_cron(self):
+ """Trigger cron run for this engine."""
+
+ rpki.log.info("Running cron for %s" % self.name)
+ rpki.https.client(privateKey = testbed_key,
+ certChain = testbed_certs,
+ x509TrustList = rpki.x509.X509_chain(self.rpkid_ta),
+ url = "https://localhost:%d/cronjob" % self.rpki_port,
+ msg = "Run cron now, please")
+
+ def run_yaml(self):
+ """Run YAML scripts for this leaf entity."""
+ rpki.log.info("Running YAML for %s" % self.name)
+ subprocess.check_call((prog_python, prog_poke, "-y", self.name + ".yaml", "-r", "list"))
+ subprocess.check_call((prog_python, prog_poke, "-y", self.name + ".yaml", "-r", "issue"))
+
+def setup_biz_cert_chain(name):
+ """Build a set of business certs."""
+ s = "exec >/dev/null 2>&1\n"
+ for kind in ("EE", "CA", "TA"):
+ d = { "name" : name,
+ "kind" : kind,
+ "ca" : "true" if kind in ("CA", "TA") else "false",
+ "openssl" : prog_openssl }
+ f = open("%(name)s-%(kind)s.cnf" % d, "w")
+ f.write(biz_cert_fmt_1 % d)
+ f.close()
+ if not os.path.exists("%(name)s-%(kind)s.key" % d):
+ s += biz_cert_fmt_2 % d
+ s += biz_cert_fmt_3 % d
+ s += (biz_cert_fmt_4 % { "name" : name, "openssl" : prog_openssl })
+ subprocess.check_call(s, shell = True)
+
+def setup_rootd(rpkid_name):
+ """Write the config files for rootd."""
+ rpki.log.info("Config files for %s" % rootd_name)
+ d = { "rootd_name" : rootd_name,
+ "rootd_port" : rootd_port,
+ "rpkid_name" : rpkid_name,
+ "rootd_sia" : rootd_sia,
+ "rsyncd_dir" : rsyncd_dir,
+ "openssl" : prog_openssl }
+ f = open(rootd_name + ".conf", "w")
+ f.write(rootd_fmt_1 % d)
+ f.close()
+ s = "exec >/dev/null 2>&1\n"
+ if not os.path.exists(rootd_name + ".key"):
+ s += rootd_fmt_2 % d
+ s += rootd_fmt_3 % d
+ subprocess.check_call(s, shell = True)
+
+def setup_rcynic():
+ """Write the config file for rcynic."""
+ rpki.log.info("Config file for rcynic")
+ d = { "rcynic_name" : rcynic_name,
+ "rootd_name" : rootd_name }
+ f = open(rcynic_name + ".conf", "w")
+ f.write(rcynic_fmt_1 % d)
+ f.close()
+
+def setup_rsyncd():
+ """Write the config file for rsyncd."""
+ rpki.log.info("Config file for rsyncd")
+ d = { "rsyncd_name" : rsyncd_name,
+ "rsyncd_port" : rsyncd_port,
+ "rsyncd_module" : rsyncd_module,
+ "rsyncd_dir" : rsyncd_dir }
+ f = open(rsyncd_name + ".conf", "w")
+ f.write(rsyncd_fmt_1 % d)
+ f.close()
+
+def setup_publication():
+ """Set up (pseudo) publication directory."""
+ rpki.log.info("Pseudo-publication directory")
+ assert rootd_sia.startswith("rsync://")
+ global rsyncd_dir
+ rsyncd_dir = os.getcwd() + "/publication/" + rootd_sia[len("rsync://"):]
+ os.makedirs(rsyncd_dir)
+
+def run_rcynic():
+ """Run rcynic to see whether what was published makes sense."""
+ rpki.log.info("Running rcynic")
+ env = os.environ.copy()
+ env["TZ"] = ""
+ subprocess.check_call((prog_rcynic, "-c", rcynic_name + ".conf"), env = env)
+ subprocess.call(rcynic_stats, shell = True, env = env)
+
+biz_cert_fmt_1 = '''\
+[ req ]
+distinguished_name = req_dn
+x509_extensions = req_x509_ext
+prompt = no
+default_md = sha256
+
+[ req_dn ]
+CN = Test Certificate %(name)s %(kind)s
+
+[ req_x509_ext ]
+basicConstraints = CA:%(ca)s
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always
+'''
+
+biz_cert_fmt_2 = '''\
+%(openssl)s genrsa -out %(name)s-%(kind)s.key 2048 &&
+'''
+
+biz_cert_fmt_3 = '''\
+%(openssl)s req -new -key %(name)s-%(kind)s.key -out %(name)s-%(kind)s.req -config %(name)s-%(kind)s.cnf &&
+'''
+
+biz_cert_fmt_4 = '''\
+%(openssl)s x509 -req -in %(name)s-TA.req -out %(name)s-TA.cer -extfile %(name)s-TA.cnf -extensions req_x509_ext -signkey %(name)s-TA.key -days 60 &&
+%(openssl)s x509 -req -in %(name)s-CA.req -out %(name)s-CA.cer -extfile %(name)s-CA.cnf -extensions req_x509_ext -CA %(name)s-TA.cer -CAkey %(name)s-TA.key -CAcreateserial &&
+%(openssl)s x509 -req -in %(name)s-EE.req -out %(name)s-EE.cer -extfile %(name)s-EE.cnf -extensions req_x509_ext -CA %(name)s-CA.cer -CAkey %(name)s-CA.key -CAcreateserial
+'''
+
+yaml_fmt_1 = '''---
+version: 1
+posturl: https://localhost:%(https_port)s/up-down/%(child_id)s
+recipient-id: "%(parent_name)s"
+sender-id: "%(my_name)s"
+
+cms-cert-file: %(my_name)s-RPKI-EE.cer
+cms-key-file: %(my_name)s-RPKI-EE.key
+cms-ca-cert-file: %(parent_name)s-RPKI-TA.cer
+cms-cert-chain-file: [ %(my_name)s-RPKI-CA.cer ]
+
+ssl-cert-file: %(my_name)s-RPKI-EE.cer
+ssl-key-file: %(my_name)s-RPKI-EE.key
+ssl-ca-cert-file: %(parent_name)s-RPKI-TA.cer
+
+requests:
+ list:
+ type: list
+ issue:
+ type: issue
+ #
+ # This is cheating, we know a priori that the class will be "1"
+ #
+ class: 1
+ sia:
+ - %(sia)s
+'''
+
+conf_fmt_1 = '''\
+
+[irdbd]
+
+startup-message = This is %(my_name)s irdbd
+
+sql-database = %(irdb_db_name)s
+sql-username = irdb
+sql-password = %(irdb_db_pass)s
+
+cms-key = %(my_name)s-IRDB-EE.key
+cms-certs.0 = %(my_name)s-IRDB-EE.cer
+cms-certs.1 = %(my_name)s-IRDB-CA.cer
+cms-ta = %(my_name)s-RPKI-TA.cer
+
+https-key = %(my_name)s-IRDB-EE.key
+https-certs.0 = %(my_name)s-IRDB-EE.cer
+https-certs.1 = %(my_name)s-IRDB-CA.cer
+
+https-url = https://localhost:%(irdb_port)d/
+
+[irbe-cli]
+
+cms-key = %(testbed_name)s-EE.key
+cms-certs.0 = %(testbed_name)s-EE.cer
+cms-certs.1 = %(testbed_name)s-CA.cer
+cms-tas = %(my_name)s-RPKI-TA.cer
+
+https-key = %(testbed_name)s-EE.key
+https-certs.0 = %(testbed_name)s-EE.cer
+https-certs.1 = %(testbed_name)s-CA.cer
+https-tas = %(my_name)s-RPKI-TA.cer
+
+https-url = https://localhost:%(rpki_port)d/left-right
+
+[rpkid]
+
+startup-message = This is %(my_name)s rpkid
+
+sql-database = %(rpki_db_name)s
+sql-username = rpki
+sql-password = %(rpki_db_pass)s
+
+cms-key = %(my_name)s-RPKI-EE.key
+cms-cert.0 = %(my_name)s-RPKI-EE.cer
+cms-cert.1 = %(my_name)s-RPKI-CA.cer
+
+cms-ta-irdb = %(my_name)s-IRDB-TA.cer
+cms-ta-irbe = %(testbed_name)s-TA.cer
+
+https-key = %(my_name)s-RPKI-EE.key
+https-cert.0 = %(my_name)s-RPKI-EE.cer
+https-cert.1 = %(my_name)s-RPKI-CA.cer
+
+https-ta = %(my_name)s-IRDB-TA.cer
+
+irdb-url = https://localhost:%(irdb_port)d/
+
+server-host = localhost
+server-port = %(rpki_port)d
+'''
+
+rootd_fmt_1 = '''\
+
+[rootd]
+
+cms-key = %(rootd_name)s-EE.key
+cms-certs.0 = %(rootd_name)s-EE.cer
+cms-certs.1 = %(rootd_name)s-CA.cer
+cms-ta = %(rpkid_name)s-RPKI-TA.cer
+
+https-key = %(rootd_name)s-EE.key
+https-certs.0 = %(rootd_name)s-EE.cer
+https-certs.1 = %(rootd_name)s-CA.cer
+
+server-port = %(rootd_port)s
+
+rootd_base = %(rootd_sia)s
+rootd_cert = %(rootd_sia)sWOMBAT.cer
+
+rpki-subject-filename = %(rsyncd_dir)sWOMBAT.cer
+
+rpki-key = %(rootd_name)s.key
+rpki-issuer = %(rootd_name)s.cer
+rpki-pkcs10-filename = %(rootd_name)s.subject.pkcs10
+
+[req]
+default_bits = 2048
+encrypt_key = no
+distinguished_name = req_dn
+req_extensions = req_x509_ext
+prompt = no
+
+[req_dn]
+CN = Completely Bogus Test Root (NOT FOR PRODUCTION USE)
+
+[req_x509_ext]
+basicConstraints = critical,CA:true
+subjectKeyIdentifier = hash
+keyUsage = critical,keyCertSign,cRLSign
+subjectInfoAccess = 1.3.6.1.5.5.7.48.5;URI:%(rootd_sia)s
+sbgp-autonomousSysNum = critical,AS:0-4294967295
+sbgp-ipAddrBlock = critical,IPv4:0.0.0.0/0,IPv6:0::/0
+'''
+
+rootd_fmt_2 = '''\
+%(openssl)s genrsa -out %(rootd_name)s.key 2048 &&
+'''
+
+rootd_fmt_3 = '''\
+%(openssl)s req -new -key %(rootd_name)s.key -out %(rootd_name)s.req -config %(rootd_name)s.conf -text &&
+%(openssl)s x509 -req -in %(rootd_name)s.req -out %(rootd_name)s.cer -outform DER -extfile %(rootd_name)s.conf -extensions req_x509_ext -signkey %(rootd_name)s.key -sha256
+'''
+
+rcynic_fmt_1 = '''\
+[rcynic]
+xml-summary = %(rcynic_name)s.xml
+jitter = 0
+use-links = yes
+use-syslog = yes
+use-stderr = yes
+log-level = log_telemetry
+trust-anchor = %(rootd_name)s.cer
+'''
+
+rsyncd_fmt_1 = '''\
+port = %(rsyncd_port)d
+address = localhost
+
+[%(rsyncd_module)s]
+read only = yes
+transfer logging = yes
+use chroot = no
+path = %(rsyncd_dir)s
+comment = RPKI test
+'''
+
+main()