rpki-pbuilder.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. #!/usr/bin/python
  2. #
  3. # $Id$
  4. #
  5. # Copyright (C) 2014 Dragon Research Labs ("DRL")
  6. # Portions copyright (C) 2013 Internet Systems Consortium ("ISC")
  7. #
  8. # Permission to use, copy, modify, and distribute this software for any
  9. # purpose with or without fee is hereby granted, provided that the above
  10. # copyright notices and this permission notice appear in all copies.
  11. #
  12. # THE SOFTWARE IS PROVIDED "AS IS" AND DRL AND ISC DISCLAIM ALL
  13. # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
  14. # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL DRL OR
  15. # ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
  16. # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
  17. # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
  18. # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
  19. # PERFORMANCE OF THIS SOFTWARE.
  20. """
  21. Debian/Ubuntu package build tool, based on pbuilder-dist and reprepro.
  22. """
  23. import os
  24. import sys
  25. import time
  26. import fcntl
  27. import errno
  28. import socket
  29. import logging
  30. import argparse
  31. import subprocess
  32. from textwrap import dedent
  33. rpki_packages = ("rpki-rp", "rpki-ca")
  34. rpki_source_package = "rpki"
  35. parser = argparse.ArgumentParser(description = __doc__,
  36. formatter_class = argparse.ArgumentDefaultsHelpFormatter)
  37. parser.add_argument("--debug", action = "store_true",
  38. help = "enable debugging code")
  39. parser.add_argument("--update-build-after", type = int, default = 7 * 24 * 60 * 60,
  40. help = "interval (in seconds) after which we should update the pbuilder environment")
  41. parser.add_argument("--lockfile", default = os.path.expanduser("~/builder.lock"),
  42. help = "avoid collisions between multiple instances of this script")
  43. parser.add_argument("--keyring", default = os.path.expanduser("~/.gnupg/pubring.gpg"),
  44. help = "PGP keyring")
  45. parser.add_argument("--svn-tree", default = os.path.expanduser("~/source/trunk/"),
  46. help = "subversion tree")
  47. parser.add_argument("--apt-tree", default = os.path.expanduser("~/repository/"),
  48. help = "reprepro repository")
  49. parser.add_argument("--apt-user", default = "aptbot",
  50. help = "username for uploading apt repository to public web server")
  51. parser.add_argument("--url-host", default = "download.rpki.net",
  52. help = "hostname of public web server")
  53. parser.add_argument("--url-scheme", default = "http",
  54. help = "URL scheme of public web server")
  55. parser.add_argument("--url-path", default = "/APT",
  56. help = "path of apt repository on public web server")
  57. parser.add_argument("--backports", nargs = "+", default = ["python-django", "python-tornado"],
  58. help = "backports needed for this build")
  59. parser.add_argument("--releases", nargs = "+", default = ["ubuntu/trusty", "debian/wheezy", "ubuntu/precise"],
  60. help = "releases for which to build")
  61. args = parser.parse_args()
  62. # Maybe logging should be configurable too. Later.
  63. logging.basicConfig(level = logging.INFO, timefmt = "%Y-%m-%dT%H:%M:%S",
  64. format = "%(asctime)s [%(process)d] %(levelname)s %(message)s")
  65. upload = socket.getfqdn() == "build-u.rpki.net"
  66. def run(*cmd, **kwargs):
  67. if args.debug:
  68. #logging.info("Running %r %r", cmd, kwargs)
  69. logging.info("Running %s", " ".join(cmd))
  70. subprocess.check_call(cmd, **kwargs)
  71. # Getting this to work right also required adding:
  72. #
  73. # DEBBUILDOPTS="-b"
  74. #
  75. # to /etc/pbuilderrc; without this, reprepro (eventually, a year after
  76. # we set this up) started failing to incorporate some of the built
  77. # packages, because the regenerated source packages had different
  78. # checksums than the ones loaded initially. See:
  79. #
  80. # http://stackoverflow.com/questions/21563872/reprepro-complains-about-the-generated-pbuilder-debian-tar-gz-archive-md5
  81. #
  82. # Putting stuff in ~/.pbuilderrc didn't work with pbuilder-dist when I
  83. # tried it last year, this may just be that sudo isn't configured to
  84. # pass HOME through, thus pbuilder is looking for ~root/.pbuilderrc.
  85. # Worth trying again at some point but not all that critical.
  86. logging.info("Starting")
  87. try:
  88. lock = os.open(args.lockfile, os.O_RDONLY | os.O_CREAT | os.O_NONBLOCK, 0666)
  89. fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
  90. except (IOError, OSError), e:
  91. sys.exit(0 if e.errno == errno.EAGAIN else "Error {!r} opening lock {!r}".format(e, args.lockfile))
  92. run("svn", "--quiet", "update", cwd = args.svn_tree)
  93. source_version = subprocess.check_output(("svnversion", "-c"), cwd = args.svn_tree).strip().split(":")[-1]
  94. logging.info("Source version is %s", source_version)
  95. if not source_version.isdigit() and not args.debug:
  96. sys.exit("Sources don't look pristine, not building ({!r})".format(source_version))
  97. source_version = "0." + source_version
  98. search_version = "_" + source_version + "~"
  99. dsc_dir = os.path.abspath(os.path.join(args.svn_tree, ".."))
  100. if not os.path.isdir(args.apt_tree):
  101. logging.info("Creating %s", args.apt_tree)
  102. os.makedirs(args.apt_tree)
  103. fn = os.path.join(args.apt_tree, "apt-gpg-key.asc")
  104. if not os.path.exists(fn):
  105. logging.info("Creating %s", fn)
  106. run("gpg", "--export", "--armor", "--keyring", args.keyring, stdout = open(fn, "w"))
  107. class Release(object):
  108. architectures = dict(amd64 = "", i386 = "-i386")
  109. releases = []
  110. packages = {}
  111. def __init__(self, distribution_release, backports):
  112. self.distribution, self.release = distribution_release.split("/")
  113. self.backports = backports
  114. self.apt_source = "{scheme}://{host}/{path}/{distribution} {release} main".format(
  115. scheme = args.url_scheme,
  116. host = args.url_host,
  117. path = args.url_path.strip("/"),
  118. distribution = self.distribution,
  119. release = self.release)
  120. self.env = dict(os.environ)
  121. if backports:
  122. self.env.update(OTHERMIRROR = "deb " + self.apt_source)
  123. self.releases.append(self)
  124. @classmethod
  125. def do_all_releases(cls):
  126. for release in cls.releases:
  127. release.setup_reprepro()
  128. for release in cls.releases:
  129. release.list_repository()
  130. for release in cls.releases:
  131. for release.arch, release.tag in cls.architectures.iteritems():
  132. release.do_one_architecture()
  133. del release.arch, release.tag
  134. @staticmethod
  135. def repokey(release, architecture, package):
  136. return (release, architecture, package)
  137. def list_repository(self):
  138. cmd = ("reprepro", "list", self.release)
  139. logging.info("Running %s", " ".join(cmd))
  140. listing = subprocess.check_output(cmd, cwd = self.tree)
  141. for line in listing.replace(":", " ").replace("|", " ").splitlines():
  142. rel, comp, arch, pkg, ver = line.split()
  143. key = (rel, arch, pkg)
  144. assert key not in self.packages
  145. self.packages[key] = ver
  146. @property
  147. def deb_in_repository(self):
  148. ret = all(self.packages.get((self.release, self.arch, package)) == self.version
  149. for package in rpki_packages)
  150. #logging.info("Got %s looking for %r in %r", ret, self.version, self.packages)
  151. return ret
  152. @property
  153. def src_in_repository(self):
  154. ret = self.packages.get((self.release, "source", rpki_source_package)) == self.version
  155. #logging.info("Got %s looking for %r in %r", ret, self.version, self.packages)
  156. return ret
  157. @property
  158. def version(self):
  159. return source_version + "~" + self.release
  160. @property
  161. def dsc(self):
  162. return os.path.join(dsc_dir, "rpki_{}.dsc".format(self.version))
  163. @property
  164. def tree(self):
  165. return os.path.join(args.apt_tree, self.distribution, "")
  166. @property
  167. def basefile(self):
  168. return os.path.expanduser("~/pbuilder/{0.release}{0.tag}-base.tgz".format(self))
  169. @property
  170. def result(self):
  171. return os.path.expanduser("~/pbuilder/{0.release}{0.tag}_result".format(self))
  172. @property
  173. def changes(self):
  174. return os.path.join(self.result, "rpki_{0.version}_{0.arch}.changes".format(self))
  175. def do_one_architecture(self):
  176. logging.info("Running build for %s %s %s", self.distribution, self.release, self.arch)
  177. if not os.path.exists(self.dsc):
  178. logging.info("%s does not exist", self.dsc)
  179. for fn in os.listdir(dsc_dir):
  180. fn = os.path.join(dsc_dir, fn)
  181. if not os.path.isdir(fn) and search_version not in fn:
  182. logging.info("Removing %s", fn)
  183. os.unlink(fn)
  184. run("rm", "-rf", "debian", cwd = args.svn_tree)
  185. logging.info("Building source package %s", self.version)
  186. run(sys.executable, "buildtools/make-version.py", cwd = args.svn_tree)
  187. run(sys.executable, "buildtools/build-debian-packages.py", "--version-suffix", self.release, cwd = args.svn_tree)
  188. run("dpkg-buildpackage", "-S", "-us", "-uc", "-rfakeroot", cwd = args.svn_tree)
  189. if not os.path.exists(self.basefile):
  190. logging.info("Creating build environment %s %s", self.release, self.arch)
  191. run("pbuilder-dist", self.release, self.arch, "create", env = self.env)
  192. elif time.time() > os.stat(self.basefile).st_mtime + args.update_build_after:
  193. logging.info("Updating build environment %s %s", self.release, self.arch)
  194. run("pbuilder-dist", self.release, self.arch, "update", env = self.env)
  195. if not self.deb_in_repository:
  196. for fn in os.listdir(self.result):
  197. fn = os.path.join(self.result, fn)
  198. logging.info("Removing %s", fn)
  199. os.unlink(fn)
  200. logging.info("Building binary packages %s %s %s", self.release, self.arch, self.version)
  201. run("pbuilder-dist", self.release, self.arch, "build", "--keyring", args.keyring, self.dsc, env = self.env)
  202. logging.info("Updating repository for %s %s %s", self.release, self.arch, self.version)
  203. run("reprepro", "--ignore=wrongdistribution", "include", self.release, self.changes, cwd = self.tree)
  204. if not self.src_in_repository:
  205. logging.info("Updating repository for %s source %s", self.release, self.version)
  206. run("reprepro", "--ignore=wrongdistribution", "includedsc", self.release, self.dsc, cwd = self.tree)
  207. def setup_reprepro(self):
  208. logging.info("Configuring reprepro for %s/%s", self.distribution, self.release)
  209. dn = os.path.join(self.tree, "conf")
  210. if not os.path.isdir(dn):
  211. logging.info("Creating %s", dn)
  212. os.makedirs(dn)
  213. fn = os.path.join(self.tree, "conf", "distributions")
  214. distributions = open(fn, "r").read() if os.path.exists(fn) else ""
  215. if "Codename: {0.release}\n".format(self) not in distributions:
  216. logging.info("%s %s", "Editing" if distributions else "Creating", fn)
  217. with open(fn, "w") as f:
  218. if distributions:
  219. f.write(distributions)
  220. f.write("\n")
  221. f.write(dedent("""\
  222. Origin: rpki.net
  223. Label: rpki.net {self.distribution} repository
  224. Codename: {self.release}
  225. Architectures: {architectures} source
  226. Components: main
  227. Description: rpki.net {Distribution} APT Repository
  228. SignWith: yes
  229. DebOverride: override.{self.release}
  230. DscOverride: override.{self.release}
  231. """.format(
  232. self = self,
  233. Distribution = self.distribution.capitalize(),
  234. architectures = " ".join(self.architectures))))
  235. fn = os.path.join(self.tree, "conf", "options")
  236. if not os.path.exists(fn):
  237. logging.info("Creating %s", fn)
  238. with open(fn, "w") as f:
  239. f.write(dedent("""\
  240. verbose
  241. ask-passphrase
  242. basedir .
  243. """))
  244. fn = os.path.join(self.tree, "conf", "override." + self.release)
  245. if not os.path.exists(fn):
  246. logging.info("Creating %s", fn)
  247. with open(fn, "w") as f:
  248. for pkg in self.backports:
  249. f.write(dedent("""\
  250. {pkg:<30} Priority optional
  251. {pkg:<30} Section python
  252. """.format(pkg = pkg)))
  253. f.write(dedent("""\
  254. rpki-ca Priority extra
  255. rpki-ca Section net
  256. rpki-rp Priority extra
  257. rpki-rp Section net
  258. """))
  259. fn = os.path.join(args.apt_tree, "rpki.{}.list".format(self.release))
  260. if not os.path.exists(fn):
  261. logging.info("Creating %s", fn)
  262. with open(fn, "w") as f:
  263. f.write(dedent("""\
  264. deb {self.apt_source}
  265. deb-src {self.apt_source}
  266. """.format(self = self)))
  267. # Load whatever releases the user specified
  268. for r in args.releases:
  269. Release(r, args.backports)
  270. # Do all the real work.
  271. Release.do_all_releases()
  272. # Upload results, maybe. We do this in two stages, to minimize the window
  273. # during which the uploaded repository might be in an inconsistent state.
  274. def rsync(*flags):
  275. cmd = ["rsync", "--archive", "--itemize-changes",
  276. "--rsh", "ssh -l {}".format(args.apt_user)]
  277. cmd.extend(flags)
  278. cmd.append(args.apt_tree)
  279. cmd.append("rsync://{host}/{path}/".format(host = args.url_host,
  280. path = args.url_path.strip("/")))
  281. if upload:
  282. logging.info("Synching repository to %s with flags %s",
  283. cmd[-1], " ".join(flags))
  284. run(*cmd)
  285. else:
  286. logging.info("Would have synched repository to %s with flags %",
  287. cmd[-1], " ".join(flags))
  288. rsync("--ignore-existing")
  289. rsync("--exclude", "HEADER.html",
  290. "--exclude", "HEADER.css",
  291. "--delete", "--delete-delay")
  292. logging.info("Done")