aboutsummaryrefslogtreecommitdiff
path: root/myrpki/yamltest.py
diff options
context:
space:
mode:
Diffstat (limited to 'myrpki/yamltest.py')
-rw-r--r--myrpki/yamltest.py438
1 files changed, 438 insertions, 0 deletions
diff --git a/myrpki/yamltest.py b/myrpki/yamltest.py
new file mode 100644
index 00000000..c9c9bb14
--- /dev/null
+++ b/myrpki/yamltest.py
@@ -0,0 +1,438 @@
+"""
+Convert testbed.py YAML configuration format to myrpki .conf and .csv
+format. Much of the YAML handling code lifted from testbed.py.
+
+$Id$
+
+Copyright (C) 2009 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 notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL 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.
+
+Portions 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.
+
+"""
+
+import subprocess, csv, re, os, getopt, sys, ConfigParser, base64, yaml, signal, errno, time
+import rpki.resource_set, rpki.sundial, myrpki
+
+section_regexp = re.compile("\s*\[\s*(.+?)\s*\]\s*$")
+variable_regexp = re.compile("\s*([-a-zA-Z0-9_]+)\s*=\s*(.+?)\s*$")
+
+def cleanpath(*names):
+ return os.path.normpath(os.path.join(*names))
+
+this_dir = os.getcwd()
+test_dir = cleanpath(this_dir, "test")
+rpkid_dir = cleanpath(this_dir, "../rpkid")
+
+prog_myirbe = cleanpath(this_dir, "myirbe.py")
+prog_myrpki = cleanpath(this_dir, "myrpki.py")
+prog_rpkid = cleanpath(rpkid_dir, "rpkid.py")
+prog_irdbd = cleanpath(rpkid_dir, "irdbd.py")
+prog_pubd = cleanpath(rpkid_dir, "pubd.py")
+prog_rootd = cleanpath(rpkid_dir, "rootd.py")
+
+prog_openssl = cleanpath(this_dir, "../openssl/openssl/apps/openssl")
+
+class roa_request(object):
+
+ def __init__(self, asn, ipv4, ipv6):
+ self.asn = asn
+ self.v4 = rpki.resource_set.roa_prefix_set_ipv4("".join(ipv4.split())) if ipv4 else None
+ self.v6 = rpki.resource_set.roa_prefix_set_ipv6("".join(ipv6.split())) if ipv6 else None
+
+ def __eq__(self, other):
+ return self.asn == other.asn and self.v4 == other.v4 and self.v6 == other.v6
+
+ def __hash__(self):
+ v4 = tuple(self.v4) if self.v4 is not None else None
+ v6 = tuple(self.v6) if self.v6 is not None else None
+ return self.asn.__hash__() + v4.__hash__() + v6.__hash__()
+
+ def __str__(self):
+ if self.v4 and self.v6:
+ return "%s: %s,%s" % (self.asn, self.v4, self.v6)
+ else:
+ return "%s: %s" % (self.asn, self.v4 or self.v6)
+
+ @classmethod
+ def parse(cls, yaml):
+ return cls(yaml.get("asn"), yaml.get("ipv4"), yaml.get("ipv6"))
+
+class allocation_db(list):
+
+ def __init__(self, yaml):
+ list.__init__(self)
+ self.root = allocation(yaml, self)
+ assert self.root.is_root()
+ if self.root.crl_interval is None:
+ self.root.crl_interval = 24 * 60 * 60
+ if self.root.regen_margin is None:
+ self.root.regen_margin = 24 * 60 * 60
+ for a in self:
+ if a.sia_base is None:
+ a.sia_base = ("rsync://rpki.example/" if a.is_root() else a.parent.sia_base) + 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
+ if a.regen_margin is None:
+ a.regen_margin = a.parent.regen_margin
+ self.root.closure()
+ self.map = dict((a.name, a) for a in self)
+ for a in self:
+ if a.is_hosted():
+ a.hosted_by = self.map[a.hosted_by]
+ a.hosted_by.hosts.append(a)
+ assert not a.is_root() and not a.hosted_by.is_hosted()
+
+ def dump(self):
+ for a in self:
+ a.dump()
+
+class allocation(object):
+
+ parent = None
+ crl_interval = None
+ regen_margin = None
+
+ base_port = 4400
+
+ @classmethod
+ def allocate_port(cls):
+ cls.base_port += 1
+ return cls.base_port
+
+ base_engine = -1
+
+ @classmethod
+ def allocate_engine(cls):
+ cls.base_engine += 1
+ return cls.base_engine
+
+ def __init__(self, yaml, db, parent = None):
+ db.append(self)
+ self.name = yaml["name"]
+ self.parent = parent
+ self.kids = [allocation(k, db, self) for k in yaml.get("kids", ())]
+ valid_until = None
+ if "valid_until" in yaml:
+ valid_until = rpki.sundial.datetime.fromdatetime(yaml.get("valid_until"))
+ if valid_until is None and "valid_for" in yaml:
+ valid_until = rpki.sundial.now() + rpki.sundial.timedelta.parse(yaml["valid_for"])
+ self.base = rpki.resource_set.resource_bag(
+ asn = 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 = rpki.sundial.timedelta.parse(yaml["crl_interval"]).convert_to_seconds()
+ if "regen_margin" in yaml:
+ self.regen_margin = rpki.sundial.timedelta.parse(yaml["regen_margin"]).convert_to_seconds()
+ self.roa_requests = [roa_request.parse(y) for y in yaml.get("roa_request", yaml.get("route_origin", ()))]
+ for r in self.roa_requests:
+ if r.v4:
+ self.base.v4 = self.base.v4.union(r.v4.to_resource_set())
+ if r.v6:
+ self.base.v6 = self.base.v6.union(r.v6.to_resource_set())
+ self.hosted_by = yaml.get("hosted_by")
+ self.hosts = []
+ if not self.is_hosted():
+ self.engine = self.allocate_engine()
+ self.rsync_port = self.allocate_port()
+ self.rpkid_port = self.allocate_port()
+ self.pubd_port = self.allocate_port()
+ self.irdbd_port = self.allocate_port()
+ if self.is_root():
+ self.rootd_port = self.allocate_port()
+
+ def closure(self):
+ resources = self.base
+ for kid in self.kids:
+ resources = resources.union(kid.closure())
+ self.resources = resources
+ return resources
+
+ def dump(self):
+ print str(self)
+
+ def __str__(self):
+ s = self.name + ":\n"
+ if self.resources.asn: s += " ASNs: %s\n" % self.resources.asn
+ 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
+ if self.is_hosted(): s += " Host: %s\n" % self.hosted_by.name
+ if self.hosts: s += " Hosts: %s\n" % ", ".join(h.name for h in self.hosts)
+ for r in self.roa_requests: s += " ROA: %s\n" % r
+ if not self.is_hosted(): s += " IPort: %s\n" % self.irdbd_port
+ if not self.is_hosted(): s += " PPort: %s\n" % self.pubd_port
+ if not self.is_hosted(): s += " RPort: %s\n" % self.rpkid_port
+ if not self.is_hosted(): s += " SPort: %s\n" % self.rsync_port
+ if self.is_root(): s += " TPort: %s\n" % self.rootd_port
+ return s + " Until: %s\n" % self.resources.valid_until
+
+ def is_root(self):
+ return self.parent is None
+
+ def is_hosted(self):
+ return self.hosted_by is not None
+
+ def path(self, *names):
+ return cleanpath(test_dir, self.name, *names)
+
+ def outfile(self, filename):
+ path = self.path(filename)
+ print "Writing", path
+ return open(path, "w")
+
+ def up_down_url(self):
+ parent_port = self.parent.hosted_by.rpkid_port if self.parent.is_hosted() else self.parent.rpkid_port
+ return "https://localhost:%d/up-down/%s/%s" % (parent_port, self.parent.name, self.name)
+
+ def dump_asns(self, fn):
+ f = self.outfile(fn)
+ for k in self.kids:
+ for a in k.resources.asn:
+ f.write("%s\t%s\n" % (k.name, a))
+ f.close()
+
+ def dump_children(self, fn):
+ f = self.outfile(fn)
+ for k in self.kids:
+ f.write("%s\t%s\t%s\n" % (k.name, k.resources.valid_until, k.path("bpki.myrpki/ca.cer")))
+ f.close()
+
+ def dump_parents(self, fn):
+ f = self.outfile(fn)
+ if self.is_root():
+ f.write("%s\t%s\t%s\n" % ("rootd", "https://localhost:%d/" % self.rootd_port, self.path("bpki.rootd/ca.cer")))
+ else:
+ f.write("%s\t%s\t%s\n" % (self.parent.name, self.up_down_url(), self.parent.path("bpki.myrpki/ca.cer")))
+ f.close()
+
+ def dump_prefixes(self, fn):
+ f = self.outfile(fn)
+ for k in self.kids:
+ for p in k.resources.v4 + k.resources.v6:
+ f.write("%s\t%s\n" % (k.name, p))
+ f.close()
+
+ def dump_roas(self, fn):
+ f = self.outfile(fn)
+ for r in self.roa_requests:
+ for p in r.v4 + r.v6 if r.v4 and r.v6 else r.v4 or r.v6 or ():
+ f.write("%s\t%s\n" % (p, r.asn))
+ f.close()
+
+ def dump_conf(self, fn):
+
+ replacements = { ("myrpki", "handle"): self.name }
+
+ if not self.is_hosted():
+ replacements.update({
+ ("myirbe", "rsync_base"): "rsync://localhost:%d/" % self.rsync_port,
+ ("myirbe", "pubd_base"): "https://localhost:%d/" % self.pubd_port,
+ ("myirbe", "rpkid_base"): "https://localhost:%d/" % self.rpkid_port,
+ ("myirbe", "irdbd_conf"): "myrpki.conf",
+ ("irdbd", "sql-database"): "irdb%d" % self.engine,
+ ("rpkid", "sql-database"): "rpki%d" % self.engine,
+ ("pubd", "sql-database"): "pubd%d" % self.engine,
+ ("irdbd", "https-url"): "https://localhost:%d/" % self.irdbd_port,
+ ("rpkid", "irdb-url"): "https://localhost:%d/" % self.irdbd_port,
+ ("rpkid", "server-port"): "%d" % self.rpkid_port,
+ ("pubd", "server-port"): "%d" % self.pubd_port,
+ ("rootd", "server-port"): "%d" % (self.rootd_port if self.is_root() else 0) })
+
+ if self.is_root():
+ replacements.update({
+ ("rootd", "server-port"): "%d" % self.rootd_port })
+
+ f = self.outfile(fn)
+ f.write("# Automatically generated, do not edit\n")
+
+ for conf in ("myrpki.conf", "rpkid.conf", "irdbd.conf", "pubd.conf", "rootd.conf"):
+ section = None
+ for line in open(conf):
+ m = section_regexp.match(line)
+ if m:
+ section = m.group(1)
+ if section is None or (self.is_hosted() and section in ("myirbe", "rpkid", "irdbd", "pubd", "rootd")):
+ continue
+ m = variable_regexp.match(line) if m is None else None
+ variable = m.group(1) if m else None
+ if (section, variable) in replacements:
+ line = variable + " = " + replacements[(section, variable)] + "\n"
+ f.write(line)
+
+ f.close()
+
+ def run_myirbe(self):
+ if not self.is_hosted():
+ print "Running myirbe.py for", self.name
+ subprocess.check_call(("python", prog_myirbe), cwd = self.path())
+
+ def run_myrpki(self):
+ print "Running myrpki.py for", self.name
+ subprocess.check_call(("python", prog_myrpki), cwd = self.path())
+
+ def run_python_daemon(self, prog):
+ basename = os.path.basename(prog)
+ p = subprocess.Popen(("python", prog, "-c", self.path("myrpki.conf")),
+ cwd = self.path(),
+ stdout = open(self.path(os.path.splitext(basename)[0] + ".log"), "w"),
+ stderr = subprocess.STDOUT)
+ print "Running %s for %s: pid %d process %r" % (basename, self.name, p.pid, p)
+ return p
+
+ def run_rpkid(self):
+ return self.run_python_daemon(prog_rpkid)
+
+ def run_irdbd(self):
+ return self.run_python_daemon(prog_irdbd)
+
+ def run_pubd(self):
+ return self.run_python_daemon(prog_pubd)
+
+ def run_rootd(self):
+ return self.run_python_daemon(prog_rootd)
+
+# Start clean
+
+for root, dirs, files in os.walk(test_dir, topdown = False):
+ for file in files:
+ os.unlink(os.path.join(root, file))
+ for dir in dirs:
+ os.rmdir(os.path.join(root, dir))
+
+# Select input file
+
+yaml_file = sys.argv[1] if len(sys.argv) > 1 else "../rpkid/testbed.1.yaml"
+
+# Read first YAML doc in file and process as compact description of
+# test layout and resource allocations. Ignore subsequent YAML docs,
+# they're for testbed.py, not this script.
+
+db = allocation_db(yaml.safe_load_all(open(yaml_file)).next())
+
+# Show what we loaded
+
+db.dump()
+
+# Set up each entity in our test
+
+for d in db:
+ os.makedirs(d.path())
+ d.dump_asns("asns.csv")
+ d.dump_children("children.csv")
+ d.dump_parents("parents.csv")
+ d.dump_prefixes("prefixes.csv")
+ d.dump_roas("roas.csv")
+ d.dump_conf("myrpki.conf")
+
+# Do initial myirbe.py run for each hosting entity to set up BPKI
+
+for d in db:
+ d.run_myirbe()
+
+# Run myrpki.py several times for each entity. First pass misses
+# stuff that isn't generated until later in first pass. Second pass
+# should pick up everything and reach a stable state. If anything
+# changes during third pass, that's a bug.
+
+for i in xrange(3):
+ for d in db:
+ d.run_myrpki()
+
+# Set up rootd's BPKI cross-certificate for its one and only child.
+
+if not os.path.exists(db.root.path("bpki.rootd/child.cer")):
+ subprocess.check_call((prog_openssl, "ca", "-notext", "-batch",
+ "-subj", "/CN=Totally Bogus BPKI Certificate For Test Purposes",
+ "-config", db.root.path("myrpki.conf"),
+ "-ss_cert", db.root.path("bpki.rpkid/ca.cer"),
+ "-out", db.root.path("bpki.rootd/child.cer"),
+ "-extensions", "ca_x509_ext_xcert0"),
+ cwd = db.root.path(),
+ env = { "PATH" : os.environ["PATH"],
+ "BPKI_DIRECTORY" : db.root.path("bpki.rootd"),
+ "RANDFILE" : ".OpenSSL.whines.unless.I.set.this" } )
+
+# Set up rootd's RPKI root certificate.
+
+if not os.path.exists(db.root.path("bpki.rootd/rpkiroot.cer")):
+ subprocess.check_call((prog_openssl, "x509", "-req", "-sha256", "-outform", "DER",
+ "-in", db.root.path("bpki.rootd/ca.req"),
+ "-signkey", db.root.path("bpki.rootd/ca.key"),
+ "-out", db.root.path("bpki.rootd/rpkiroot.cer"),
+ "-extfile", db.root.path("myrpki.conf"),
+ "-extensions", "rpki_x509_extensions"),
+ cwd = db.root.path(),
+ env = { "PATH" : os.environ["PATH"],
+ "BPKI_DIRECTORY" : db.root.path("bpki.rootd"),
+ "RANDFILE" : ".OpenSSL.whines.unless.I.set.this" } )
+
+# At this point we need to start a whole lotta daemons.
+
+progs = []
+
+try:
+ print "Running daemons"
+ progs.append(db.root.run_rootd())
+ progs.extend(d.run_irdbd() for d in db if not d.is_hosted())
+ progs.extend(d.run_pubd() for d in db if not d.is_hosted())
+ progs.extend(d.run_rpkid() for d in db if not d.is_hosted())
+
+ print "Giving daemons time to start up"
+ time.sleep(20)
+
+ # Run myirbe again for each host, to set up IRDB and RPKI objects.
+ # Need to run a second time to push BSC certs out to rpkid.
+ # Nothing should happen on the third pass.
+
+ for i in xrange(3):
+ for d in db:
+ d.run_myirbe()
+
+ print "Done initializing daemons"
+
+ # Wait until something terminates.
+
+ signal.signal(signal.SIGCHLD, lambda *dont_care: None)
+ if not [p for p in progs if p.poll() is not None]:
+ signal.pause()
+
+finally:
+
+ # Shut everything down.
+
+ signal.signal(signal.SIGCHLD, signal.SIG_DFL)
+ for p in progs:
+ if p.poll() is None:
+ os.kill(p.pid, signal.SIGTERM)
+ print "Program pid %d %r returned %d" % (p.pid, p, p.wait())