aboutsummaryrefslogtreecommitdiff
path: root/myrpki.rototill/myirbe.py
diff options
context:
space:
mode:
authorRob Austein <sra@hactrn.net>2010-01-22 21:55:09 +0000
committerRob Austein <sra@hactrn.net>2010-01-22 21:55:09 +0000
commit6c7445cce4c8a05c3e762fd1d537c54d6ea1fc8c (patch)
tree42c28e2d42fe56e7b069230949246dd29f06944c /myrpki.rototill/myirbe.py
parent502df88d20c6e0abd56235d4e16de14cca70dd1a (diff)
Branch myrpki code before starting rototill based on feedback from
Tokyo workshop. svn path=/myrpki.rototill; revision=2958
Diffstat (limited to 'myrpki.rototill/myirbe.py')
-rw-r--r--myrpki.rototill/myirbe.py549
1 files changed, 549 insertions, 0 deletions
diff --git a/myrpki.rototill/myirbe.py b/myrpki.rototill/myirbe.py
new file mode 100644
index 00000000..ad54c9aa
--- /dev/null
+++ b/myrpki.rototill/myirbe.py
@@ -0,0 +1,549 @@
+"""
+IRBE-side stuff for myrpki tools.
+
+The basic model here is that each entity with resources to certify
+runs the myrpki tool, but not all of them necessarily run their own
+RPKi engines. The entities that do run RPKI engines get data from the
+entities they host via the XML files output by the myrpki tool. Those
+XML files are the input to this script, which uses them to do all the
+work of constructing certificates, populating SQL databases, and so
+forth. A few operations (eg, BSC construction) generate data which
+has to be shipped back to the resource holder, which we do by updating
+the same XML file.
+
+In essence, the XML files are a sneakernet (or email, or carrier
+pigeon) communication channel between the resource holders and the
+RPKI engine operators.
+
+As a convenience, for the normal case where the RPKI engine operator
+is itself a resource holder, this script also runs the myrpki script
+directly to process the RPKI engine operator's own resources.
+
+Note that, due to the back and forth nature of some of these
+operations, it may take several cycles for data structures to stablize
+and everything to reach a steady state. This is normal.
+
+
+$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.
+"""
+
+from __future__ import with_statement
+
+import lxml.etree, base64, subprocess, sys, os, time, re, getopt, warnings
+import rpki.https, rpki.config, rpki.resource_set, rpki.relaxng
+import rpki.exceptions, rpki.left_right, rpki.log, rpki.x509, rpki.async
+import myrpki, schema
+
+# Silence warning while loading MySQLdb in Python 2.6, sigh
+if hasattr(warnings, "catch_warnings"):
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ import MySQLdb
+else:
+ import MySQLdb
+
+def tag(t):
+ """
+ Wrap an element name in the right XML namespace goop.
+ """
+ return "{http://www.hactrn.net/uris/rpki/myrpki/}" + t
+
+def findbase64(tree, name, b64type = rpki.x509.X509):
+ """
+ Find and extract a base64-encoded XML element, if present.
+ """
+ x = tree.findtext(tag(name))
+ return b64type(Base64 = x) if x else None
+
+# For simple cases we don't really care what these value are, so long
+# as we're consistant about them, so wiring them in is fine.
+
+bsc_handle = "bsc"
+repository_handle = "repository"
+
+class caller(object):
+ """
+ Handle client-side mechanics for left-right and publication
+ protocols.
+ """
+
+ debug = True
+
+ def __init__(self, proto, client_key, client_cert, server_ta, server_cert, url):
+ self.proto = proto
+ self.client_key = client_key
+ self.client_cert = client_cert
+ self.server_ta = server_ta
+ self.server_cert = server_cert
+ self.url = url
+
+ def __call__(self, cb, eb, pdus):
+
+ def done(cms):
+ msg, xml = self.proto.cms_msg.unwrap(cms, (self.server_ta, self.server_cert), pretty_print = True)
+ if self.debug:
+ print "Reply:", xml
+ cb(msg)
+
+ msg = self.proto.msg.query(*pdus)
+ cms, xml = self.proto.cms_msg.wrap(msg, self.client_key, self.client_cert, pretty_print = True)
+ if self.debug:
+ print "Query:", xml
+
+ rpki.https.client(
+ client_key = self.client_key,
+ client_cert = self.client_cert,
+ server_ta = self.server_ta,
+ url = self.url,
+ msg = cms,
+ callback = done,
+ errback = eb)
+
+os.environ["TZ"] = "UTC"
+time.tzset()
+
+rpki.log.init("myirbe")
+
+cfg_file = "myrpki.conf"
+
+bpki_only = False
+
+opts, argv = getopt.getopt(sys.argv[1:], "bc:h?", ["bpki_only", "config=", "help"])
+for o, a in opts:
+ if o in ("-b", "--bpki_only"):
+ bpki_only = True
+ elif o in ("-c", "--config"):
+ cfg_file = a
+ elif o in ("-h", "--help", "-?"):
+ print __doc__
+ sys.exit(0)
+
+cfg = rpki.config.parser(cfg_file, "myirbe")
+
+cfg.set_global_flags()
+
+myrpki.openssl = cfg.get("openssl", "openssl", "myrpki")
+
+handle = cfg.get("handle", cfg.get("handle", "Amnesiac", "myrpki"))
+
+want_pubd = cfg.getboolean("want_pubd", False)
+want_rootd = cfg.getboolean("want_rootd", False)
+
+bpki_modified = False
+
+bpki = myrpki.CA(cfg_file, cfg.get("bpki_directory"))
+bpki_modified |= bpki.setup(cfg.get("bpki_ta_dn", "/CN=%s BPKI TA" % handle))
+bpki_modified |= bpki.ee( cfg.get("bpki_rpkid_ee_dn", "/CN=%s rpkid EE" % handle), "rpkid")
+bpki_modified |= bpki.ee( cfg.get("bpki_irdbd_ee_dn", "/CN=%s irdbd EE" % handle), "irdbd")
+bpki_modified |= bpki.ee( cfg.get("bpki_irbe_ee_dn", "/CN=%s irbe EE" % handle), "irbe")
+if want_pubd:
+ bpki_modified |= bpki.ee( cfg.get("bpki_pubd_ee_dn", "/CN=%s pubd EE" % handle), "pubd")
+if want_rootd:
+ bpki_modified |= bpki.ee( cfg.get("bpki_rootd_ee_dn", "/CN=%s rootd EE" % handle), "rootd")
+
+if bpki_modified:
+ print "BPKI (re)initialized. You need to (re)start daemons before continuing."
+
+if bpki_modified or bpki_only:
+ sys.exit()
+
+# Default values for CRL parameters are very low, for testing.
+
+self_crl_interval = cfg.getint("self_crl_interval", 900)
+self_regen_margin = cfg.getint("self_regen_margin", 300)
+pubd_base = cfg.get("pubd_base").rstrip("/") + "/"
+rpkid_base = cfg.get("rpkid_base").rstrip("/") + "/"
+
+# Nasty regexp for parsing rpkid's up-down service URLs.
+
+updown_regexp = re.compile(re.escape(rpkid_base) + "up-down/([-A-Z0-9_]+)/([-A-Z0-9_]+)$", re.I)
+
+# Wrappers to simplify calling rpkid and pubd.
+
+call_rpkid = rpki.async.sync_wrapper(caller(
+ proto = rpki.left_right,
+ client_key = rpki.x509.RSA( PEM_file = bpki.dir + "/irbe.key"),
+ client_cert = rpki.x509.X509(PEM_file = bpki.dir + "/irbe.cer"),
+ server_ta = rpki.x509.X509(PEM_file = bpki.cer),
+ server_cert = rpki.x509.X509(PEM_file = bpki.dir + "/rpkid.cer"),
+ url = rpkid_base + "left-right"))
+
+if want_pubd:
+
+ call_pubd = rpki.async.sync_wrapper(caller(
+ proto = rpki.publication,
+ client_key = rpki.x509.RSA( PEM_file = bpki.dir + "/irbe.key"),
+ client_cert = rpki.x509.X509(PEM_file = bpki.dir + "/irbe.cer"),
+ server_ta = rpki.x509.X509(PEM_file = bpki.cer),
+ server_cert = rpki.x509.X509(PEM_file = bpki.dir + "/pubd.cer"),
+ url = pubd_base + "control"))
+
+ # Make sure that pubd's BPKI CRL is up to date.
+
+ call_pubd((rpki.publication.config_elt.make_pdu(
+ action = "set",
+ bpki_crl = rpki.x509.CRL(PEM_file = bpki.crl)),))
+
+irdbd_cfg = rpki.config.parser(cfg.get("irdbd_conf", cfg_file), "irdbd")
+
+db = MySQLdb.connect(user = irdbd_cfg.get("sql-username"),
+ db = irdbd_cfg.get("sql-database"),
+ passwd = irdbd_cfg.get("sql-password"))
+
+cur = db.cursor()
+
+xmlfiles = []
+
+# If [myrpki] section is present in config file, run myrpki.py
+# internally, as a convenience, and include its output at the head of
+# our list of XML files to process.
+
+if cfg.has_section("myrpki"):
+ myrpki.main(("-c", cfg_file))
+ my_xmlfile = cfg.get("xml_filename", None, "myrpki")
+ assert my_xmlfile is not None
+ xmlfiles.append(my_xmlfile)
+
+# Add any other XML files specified on the command line
+
+xmlfiles.extend(argv)
+
+my_handle = None
+
+for xmlfile in xmlfiles:
+
+ # Parse XML file and validate it against our scheme
+
+ tree = lxml.etree.parse(xmlfile).getroot()
+ try:
+ schema.myrpki.assertValid(tree)
+ except lxml.etree.DocumentInvalid:
+ print lxml.etree.tostring(tree, pretty_print = True)
+ raise
+
+ handle = tree.get("handle")
+
+ if xmlfile == my_xmlfile:
+ my_handle = handle
+
+ # Update IRDB with parsed resource and roa-request data.
+
+ cur.execute(
+ """
+ DELETE
+ FROM roa_request_prefix
+ USING roa_request, roa_request_prefix
+ WHERE roa_request.roa_request_id = roa_request_prefix.roa_request_id AND roa_request.roa_request_handle = %s
+ """, (handle,))
+
+ cur.execute("DELETE FROM roa_request WHERE roa_request.roa_request_handle = %s", (handle,))
+
+ for x in tree.getiterator(tag("roa_request")):
+ cur.execute("INSERT roa_request (roa_request_handle, asn) VALUES (%s, %s)", (handle, x.get("asn")))
+ roa_request_id = cur.lastrowid
+ for version, prefix_set in ((4, rpki.resource_set.roa_prefix_set_ipv4(x.get("v4"))), (6, rpki.resource_set.roa_prefix_set_ipv6(x.get("v6")))):
+ if prefix_set:
+ cur.executemany("INSERT roa_request_prefix (roa_request_id, prefix, prefixlen, max_prefixlen, version) VALUES (%s, %s, %s, %s, %s)",
+ ((roa_request_id, p.prefix, p.prefixlen, p.max_prefixlen, version) for p in prefix_set))
+
+ cur.execute(
+ """
+ DELETE
+ FROM registrant_asn
+ USING registrant, registrant_asn
+ WHERE registrant.registrant_id = registrant_asn.registrant_id AND registrant.registry_handle = %s
+ """ , (handle,))
+
+ cur.execute(
+ """
+ DELETE FROM registrant_net USING registrant, registrant_net
+ WHERE registrant.registrant_id = registrant_net.registrant_id AND registrant.registry_handle = %s
+ """ , (handle,))
+
+ cur.execute("DELETE FROM registrant WHERE registrant.registry_handle = %s" , (handle,))
+
+ for x in tree.getiterator(tag("child")):
+ child_handle = x.get("handle")
+ asns = rpki.resource_set.resource_set_as(x.get("asns"))
+ ipv4 = rpki.resource_set.resource_set_ipv4(x.get("v4"))
+ ipv6 = rpki.resource_set.resource_set_ipv6(x.get("v6"))
+
+ cur.execute("INSERT registrant (registrant_handle, registry_handle, registrant_name, valid_until) VALUES (%s, %s, %s, %s)",
+ (child_handle, handle, child_handle, rpki.sundial.datetime.fromXMLtime(x.get("valid_until")).to_sql()))
+ child_id = cur.lastrowid
+ if asns:
+ cur.executemany("INSERT registrant_asn (start_as, end_as, registrant_id) VALUES (%s, %s, %s)",
+ ((a.min, a.max, child_id) for a in asns))
+ if ipv4:
+ cur.executemany("INSERT registrant_net (start_ip, end_ip, version, registrant_id) VALUES (%s, %s, 4, %s)",
+ ((a.min, a.max, child_id) for a in ipv4))
+ if ipv6:
+ cur.executemany("INSERT registrant_net (start_ip, end_ip, version, registrant_id) VALUES (%s, %s, 6, %s)",
+ ((a.min, a.max, child_id) for a in ipv6))
+
+ db.commit()
+
+ # Check for certificates before attempting anything else
+
+ hosted_cacert = findbase64(tree, "bpki_ca_certificate")
+ if not hosted_cacert:
+ print "Nothing else I can do without a trust anchor for the entity I'm hosting."
+ continue
+
+ rpkid_xcert = rpki.x509.X509(PEM_file = bpki.fxcert(handle + ".cacert.cer",
+ hosted_cacert.get_PEM(),
+ path_restriction = 1))
+
+ # See what rpkid and pubd already have on file for this entity.
+
+ if want_pubd:
+ client_pdus = dict((x.client_handle, x)
+ for x in call_pubd((rpki.publication.client_elt.make_pdu(action = "list"),))
+ if isinstance(x, rpki.publication.client_elt))
+
+ rpkid_reply = call_rpkid((
+ rpki.left_right.self_elt.make_pdu( action = "get", tag = "self", self_handle = handle),
+ rpki.left_right.bsc_elt.make_pdu( action = "list", tag = "bsc", self_handle = handle),
+ rpki.left_right.repository_elt.make_pdu(action = "list", tag = "repository", self_handle = handle),
+ rpki.left_right.parent_elt.make_pdu( action = "list", tag = "parent", self_handle = handle),
+ rpki.left_right.child_elt.make_pdu( action = "list", tag = "child", self_handle = handle)))
+
+ self_pdu = rpkid_reply[0]
+ bsc_pdus = dict((x.bsc_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.bsc_elt))
+ repository_pdus = dict((x.repository_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.repository_elt))
+ parent_pdus = dict((x.parent_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.parent_elt))
+ child_pdus = dict((x.child_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.child_elt))
+
+ pubd_query = []
+ rpkid_query = []
+
+ # There should be exactly one <self/> object per hosted entity, by definition
+
+ if (isinstance(self_pdu, rpki.left_right.report_error_elt) or
+ self_pdu.crl_interval != self_crl_interval or
+ self_pdu.regen_margin != self_regen_margin or
+ self_pdu.bpki_cert != rpkid_xcert):
+ rpkid_query.append(rpki.left_right.self_elt.make_pdu(
+ action = "create" if isinstance(self_pdu, rpki.left_right.report_error_elt) else "set",
+ tag = "self",
+ self_handle = handle,
+ bpki_cert = rpkid_xcert,
+ crl_interval = self_crl_interval,
+ regen_margin = self_regen_margin))
+
+ # In general we only need one <bsc/> per <self/>. BSC objects are a
+ # little unusual in that the PKCS #10 subelement is generated by rpkid
+ # in response to generate_keypair, so there's more of a separation
+ # between create and set than with other objects.
+
+ bsc_cert = findbase64(tree, "bpki_bsc_certificate")
+ bsc_crl = findbase64(tree, "bpki_crl", rpki.x509.CRL)
+
+ bsc_pdu = bsc_pdus.pop(bsc_handle, None)
+
+ if bsc_pdu is None:
+ rpkid_query.append(rpki.left_right.bsc_elt.make_pdu(
+ action = "create",
+ tag = "bsc",
+ self_handle = handle,
+ bsc_handle = bsc_handle,
+ generate_keypair = "yes"))
+ elif bsc_pdu.signing_cert != bsc_cert or bsc_pdu.signing_cert_crl != bsc_crl:
+ rpkid_query.append(rpki.left_right.bsc_elt.make_pdu(
+ action = "set",
+ tag = "bsc",
+ self_handle = handle,
+ bsc_handle = bsc_handle,
+ signing_cert = bsc_cert,
+ signing_cert_crl = bsc_crl))
+
+ rpkid_query.extend(rpki.left_right.bsc_elt.make_pdu(
+ action = "destroy", self_handle = handle, bsc_handle = b) for b in bsc_pdus)
+
+ bsc_req = None
+
+ if bsc_pdu and bsc_pdu.pkcs10_request:
+ bsc_req = bsc_pdu.pkcs10_request
+
+ # In general we need one <repository/> per publication daemon with
+ # whom this <self/> has a relationship. In practice there is rarely
+ # (never?) a good reason for a single <self/> to use multiple
+ # publication services, so in normal use we only need one
+ # <repository/> object. If for some reason you really need more
+ # than this, you'll have to hack.
+
+ repository_cert = findbase64(tree, "bpki_repository_certificate")
+ if repository_cert:
+
+ repository_pdu = repository_pdus.pop(repository_handle, None)
+ repository_uri = pubd_base + "client/" + tree.get("repository_handle")
+
+ if (repository_pdu is None or
+ repository_pdu.bsc_handle != bsc_handle or
+ repository_pdu.peer_contact_uri != repository_uri or
+ repository_pdu.bpki_cert != repository_cert):
+ rpkid_query.append(rpki.left_right.repository_elt.make_pdu(
+ action = "create" if repository_pdu is None else "set",
+ tag = repository_handle,
+ self_handle = handle,
+ repository_handle = repository_handle,
+ bsc_handle = bsc_handle,
+ peer_contact_uri = repository_uri,
+ bpki_cert = repository_cert))
+
+ rpkid_query.extend(rpki.left_right.repository_elt.make_pdu(
+ action = "destroy", self_handle = handle, repository_handle = r) for r in repository_pdus)
+
+ # <parent/> setup code here used to be ridiculously complex. Most
+ # of the insanity was due to a misguided attempt to deduce pubd
+ # setup from other data; now that pubd setup is driven by
+ # pubclients.csv, parent setup should be relatively straightforward,
+ # but beware of lingering excessive cleverness in anything dealing
+ # with parent objects in this script.
+
+ for parent in tree.getiterator(tag("parent")):
+
+ parent_handle = parent.get("handle")
+ parent_pdu = parent_pdus.pop(parent_handle, None)
+ parent_uri = parent.get("service_uri")
+ parent_myhandle = parent.get("myhandle")
+ parent_sia_base = parent.get("sia_base")
+ parent_cms_cert = findbase64(parent, "bpki_cms_certificate")
+ parent_https_cert = findbase64(parent, "bpki_https_certificate")
+
+ if (parent_pdu is None or
+ parent_pdu.bsc_handle != bsc_handle or
+ parent_pdu.repository_handle != repository_handle or
+ parent_pdu.peer_contact_uri != parent_uri or
+ parent_pdu.sia_base != parent_sia_base or
+ parent_pdu.sender_name != parent_myhandle or
+ parent_pdu.recipient_name != parent_handle or
+ parent_pdu.bpki_cms_cert != parent_cms_cert or
+ parent_pdu.bpki_https_cert != parent_https_cert):
+ rpkid_query.append(rpki.left_right.parent_elt.make_pdu(
+ action = "create" if parent_pdu is None else "set",
+ tag = parent_handle,
+ self_handle = handle,
+ parent_handle = parent_handle,
+ bsc_handle = bsc_handle,
+ repository_handle = repository_handle,
+ peer_contact_uri = parent_uri,
+ sia_base = parent_sia_base,
+ sender_name = parent_myhandle,
+ recipient_name = parent_handle,
+ bpki_cms_cert = parent_cms_cert,
+ bpki_https_cert = parent_https_cert))
+
+ rpkid_query.extend(rpki.left_right.parent_elt.make_pdu(
+ action = "destroy", self_handle = handle, parent_handle = p) for p in parent_pdus)
+
+ # Children are simpler than parents, because they call us, so no URL
+ # to construct and figuring out what certificate to use is their
+ # problem, not ours.
+
+ for child in tree.getiterator(tag("child")):
+
+ child_handle = child.get("handle")
+ child_pdu = child_pdus.pop(child_handle, None)
+ child_cert = findbase64(child, "bpki_certificate")
+
+ if (child_pdu is None or
+ child_pdu.bsc_handle != bsc_handle or
+ child_pdu.bpki_cert != child_cert):
+ rpkid_query.append(rpki.left_right.child_elt.make_pdu(
+ action = "create" if child_pdu is None else "set",
+ tag = child_handle,
+ self_handle = handle,
+ child_handle = child_handle,
+ bsc_handle = bsc_handle,
+ bpki_cert = child_cert))
+
+ rpkid_query.extend(rpki.left_right.child_elt.make_pdu(
+ action = "destroy", self_handle = handle, child_handle = c) for c in child_pdus)
+
+ # Publication setup, used to be inferred (badly) from parent setup,
+ # now handled explictly via yet another freaking .csv file.
+
+ if want_pubd:
+
+ for client_handle, client_bpki_cert, client_base_uri in myrpki.csv_open(cfg.get("pubclients_csv", "pubclients.csv")):
+
+ if os.path.exists(client_bpki_cert):
+
+ client_pdu = client_pdus.pop(client_handle, None)
+
+ client_bpki_cert = rpki.x509.X509(PEM_file = bpki.xcert(client_bpki_cert))
+
+ if (client_pdu is None or
+ client_pdu.base_uri != client_base_uri or
+ client_pdu.bpki_cert != client_bpki_cert):
+ pubd_query.append(rpki.publication.client_elt.make_pdu(
+ action = "create" if client_pdu is None else "set",
+ client_handle = client_handle,
+ bpki_cert = client_bpki_cert,
+ base_uri = client_base_uri))
+
+ pubd_query.extend(rpki.publication.client_elt.make_pdu(
+ action = "destroy", client_handle = p) for p in client_pdus)
+
+ # If we changed anything, ship updates off to daemons
+
+ if rpkid_query:
+ rpkid_reply = call_rpkid(rpkid_query)
+ bsc_pdus = dict((x.bsc_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.bsc_elt))
+ if bsc_handle in bsc_pdus and bsc_pdus[bsc_handle].pkcs10_request:
+ bsc_req = bsc_pdus[bsc_handle].pkcs10_request
+ for r in rpkid_reply:
+ assert not isinstance(r, rpki.left_right.report_error_elt)
+
+ if pubd_query:
+ assert want_pubd
+ pubd_reply = call_pubd(pubd_query)
+ for r in pubd_reply:
+ assert not isinstance(r, rpki.publication.report_error_elt)
+
+ # Rewrite XML.
+
+ e = tree.find(tag("bpki_bsc_pkcs10"))
+ if e is None and bsc_req is not None:
+ e = lxml.etree.SubElement(tree, "bpki_bsc_pkcs10")
+ elif bsc_req is None:
+ tree.remove(e)
+
+ if bsc_req is not None:
+ assert e is not None
+ e.text = bsc_req.get_Base64()
+
+ # Something weird going on here with lxml linked against recent
+ # versions of libxml2. Looks like modifying the tree above somehow
+ # produces validation errors, but it works fine if we convert it to
+ # a string and parse it again. I'm not seeing any problems with any
+ # of the other code that uses lxml to do validation, just this one
+ # place. Weird. Kludge around it for now.
+
+ tree = lxml.etree.fromstring(lxml.etree.tostring(tree))
+
+ try:
+ schema.myrpki.assertValid(tree)
+ except lxml.etree.DocumentInvalid:
+ print lxml.etree.tostring(tree, pretty_print = True)
+ raise
+
+ lxml.etree.ElementTree(tree).write(xmlfile + ".tmp", pretty_print = True)
+ os.rename(xmlfile + ".tmp", xmlfile)
+
+db.close()