rpki-pbuilder.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  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("--git-tree", default = os.path.expanduser("~/source/master/"),
  46. help = "git tree")
  47. parser.add_argument("--apt-tree", default = os.path.expanduser("~/repository/"),
  48. help = "reprepro repository")
  49. parser.add_argument("--url-host", default = "download.rpki.net",
  50. help = "hostname of public web server")
  51. parser.add_argument("--url-scheme", default = "http",
  52. help = "URL scheme of public web server")
  53. parser.add_argument("--url-path", default = "/APT",
  54. help = "path of apt repository on public web server")
  55. parser.add_argument("--backports", nargs = "+", default = ["python-django", "python-tornado"],
  56. help = "backports needed for this build")
  57. parser.add_argument("--releases", nargs = "+", default = ["debian/jessie", "ubuntu/xenial"],
  58. help = "releases for which to build")
  59. args = parser.parse_args()
  60. # Maybe logging should be configurable too. Later.
  61. logging.basicConfig(level = logging.INFO, timefmt = "%Y-%m-%dT%H:%M:%S",
  62. format = "%(asctime)s [%(process)d] %(levelname)s %(message)s")
  63. upload = socket.getfqdn() == "build-u.rpki.net"
  64. def run(*cmd, **kwargs):
  65. if args.debug:
  66. #logging.info("Running %r %r", cmd, kwargs)
  67. logging.info("Running %s", " ".join(cmd))
  68. subprocess.check_call(cmd, **kwargs)
  69. # Getting this to work right also required adding:
  70. #
  71. # DEBBUILDOPTS="-b"
  72. #
  73. # to /etc/pbuilderrc; without this, reprepro (eventually, a year after
  74. # we set this up) started failing to incorporate some of the built
  75. # packages, because the regenerated source packages had different
  76. # checksums than the ones loaded initially. See:
  77. #
  78. # http://stackoverflow.com/questions/21563872/reprepro-complains-about-the-generated-pbuilder-debian-tar-gz-archive-md5
  79. #
  80. # Putting stuff in ~/.pbuilderrc didn't work with pbuilder-dist when I
  81. # tried it last year, this may just be that sudo isn't configured to
  82. # pass HOME through, thus pbuilder is looking for ~root/.pbuilderrc.
  83. # Worth trying again at some point but not all that critical.
  84. logging.info("Starting")
  85. try:
  86. lock = os.open(args.lockfile, os.O_RDONLY | os.O_CREAT | os.O_NONBLOCK, 0666)
  87. fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
  88. except (IOError, OSError), e:
  89. sys.exit(0 if e.errno == errno.EAGAIN else "Error {!r} opening lock {!r}".format(e, args.lockfile))
  90. run("git", "fetch", "--all", "--prune", cwd = args.git_tree)
  91. run("git", "pull", cwd = args.git_tree)
  92. run("git", "verify-commit", "HEAD", cwd = args.git_tree)
  93. source_version = subprocess.check_output((sys.executable, os.path.join(args.git_tree, "buildtools/make-version.py"),
  94. "--build-tag", "--stdout"), cwd = args.git_tree).strip()
  95. assert source_version.startswith("buildbot-")
  96. source_version = source_version[len("buildbot-"):].replace("-", ".")
  97. logging.info("Source version is %s", source_version)
  98. if not args.debug:
  99. try:
  100. run("git", "diff-index", "--quiet", "HEAD", cwd = args.git_tree)
  101. except subprocess.CalledProcessError:
  102. sys.exit("Sources don't look pristine, not building ({!r})".format(source_version))
  103. search_version = "_" + source_version + "~"
  104. dsc_dir = os.path.abspath(os.path.join(args.git_tree, ".."))
  105. if not os.path.isdir(args.apt_tree):
  106. logging.info("Creating %s", args.apt_tree)
  107. os.makedirs(args.apt_tree)
  108. fn = os.path.join(args.apt_tree, "apt-gpg-key.asc")
  109. if not os.path.exists(fn):
  110. logging.info("Creating %s", fn)
  111. run("gpg", "--export", "--armor", "--keyring", args.keyring, stdout = open(fn, "w"))
  112. class Release(object):
  113. architectures = dict(amd64 = "", i386 = "-i386")
  114. releases = []
  115. packages = {}
  116. def __init__(self, distribution_release, backports):
  117. self.distribution, self.release = distribution_release.split("/")
  118. self.backports = backports
  119. self.apt_source = "{scheme}://{host}/{path}/{distribution} {release} main".format(
  120. scheme = args.url_scheme,
  121. host = args.url_host,
  122. path = args.url_path.strip("/"),
  123. distribution = self.distribution,
  124. release = self.release)
  125. self.env = dict(os.environ)
  126. if backports:
  127. self.env.update(OTHERMIRROR = "deb " + self.apt_source)
  128. self.releases.append(self)
  129. @classmethod
  130. def do_all_releases(cls):
  131. for release in cls.releases:
  132. release.setup_reprepro()
  133. for release in cls.releases:
  134. release.list_repository()
  135. for release in cls.releases:
  136. for release.arch, release.tag in cls.architectures.iteritems():
  137. release.do_one_architecture()
  138. del release.arch, release.tag
  139. @staticmethod
  140. def repokey(release, architecture, package):
  141. return (release, architecture, package)
  142. def list_repository(self):
  143. cmd = ("reprepro", "list", self.release)
  144. logging.info("Running %s", " ".join(cmd))
  145. listing = subprocess.check_output(cmd, cwd = self.tree)
  146. for line in listing.replace(":", " ").replace("|", " ").splitlines():
  147. rel, comp, arch, pkg, ver = line.split()
  148. key = (rel, arch, pkg)
  149. assert key not in self.packages
  150. self.packages[key] = ver
  151. @property
  152. def deb_in_repository(self):
  153. ret = all(self.packages.get((self.release, self.arch, package)) == self.version
  154. for package in rpki_packages)
  155. #logging.info("Got %s looking for %r in %r", ret, self.version, self.packages)
  156. return ret
  157. @property
  158. def src_in_repository(self):
  159. ret = self.packages.get((self.release, "source", rpki_source_package)) == self.version
  160. #logging.info("Got %s looking for %r in %r", ret, self.version, self.packages)
  161. return ret
  162. @property
  163. def version(self):
  164. return source_version + "~" + self.release
  165. @property
  166. def dsc(self):
  167. return os.path.join(dsc_dir, "rpki_{}.dsc".format(self.version))
  168. @property
  169. def tree(self):
  170. return os.path.join(args.apt_tree, self.distribution, "")
  171. @property
  172. def basefile(self):
  173. return os.path.expanduser("~/pbuilder/{0.release}{0.tag}-base.tgz".format(self))
  174. @property
  175. def result(self):
  176. return os.path.expanduser("~/pbuilder/{0.release}{0.tag}_result".format(self))
  177. @property
  178. def changes(self):
  179. return os.path.join(self.result, "rpki_{0.version}_{0.arch}.changes".format(self))
  180. def do_one_architecture(self):
  181. logging.info("Running build for %s %s %s", self.distribution, self.release, self.arch)
  182. if not os.path.exists(self.dsc):
  183. logging.info("%s does not exist", self.dsc)
  184. for fn in os.listdir(dsc_dir):
  185. fn = os.path.join(dsc_dir, fn)
  186. if not os.path.isdir(fn) and search_version not in fn:
  187. logging.info("Removing %s", fn)
  188. os.unlink(fn)
  189. run("rm", "-rf", "debian", cwd = args.git_tree)
  190. logging.info("Building source package %s", self.version)
  191. run(sys.executable, "buildtools/build-debian-packages.py", "--version-suffix", self.release, cwd = args.git_tree)
  192. run("dpkg-buildpackage", "-S", "-us", "-uc", "-rfakeroot", cwd = args.git_tree)
  193. if not os.path.exists(self.basefile):
  194. logging.info("Creating build environment %s %s", self.release, self.arch)
  195. run("pbuilder-dist", self.release, self.arch, "create", env = self.env)
  196. elif time.time() > os.stat(self.basefile).st_mtime + args.update_build_after:
  197. logging.info("Updating build environment %s %s", self.release, self.arch)
  198. run("pbuilder-dist", self.release, self.arch, "update", env = self.env)
  199. if not self.deb_in_repository:
  200. for fn in os.listdir(self.result):
  201. fn = os.path.join(self.result, fn)
  202. logging.info("Removing %s", fn)
  203. os.unlink(fn)
  204. logging.info("Building binary packages %s %s %s", self.release, self.arch, self.version)
  205. run("pbuilder-dist", self.release, self.arch, "build", "--keyring", args.keyring, self.dsc, env = self.env)
  206. logging.info("Updating repository for %s %s %s", self.release, self.arch, self.version)
  207. run("reprepro", "--ignore=wrongdistribution", "include", self.release, self.changes, cwd = self.tree)
  208. if not self.src_in_repository:
  209. logging.info("Updating repository for %s source %s", self.release, self.version)
  210. run("reprepro", "--ignore=wrongdistribution", "includedsc", self.release, self.dsc, cwd = self.tree)
  211. def setup_reprepro(self):
  212. logging.info("Configuring reprepro for %s/%s", self.distribution, self.release)
  213. dn = os.path.join(self.tree, "conf")
  214. if not os.path.isdir(dn):
  215. logging.info("Creating %s", dn)
  216. os.makedirs(dn)
  217. fn = os.path.join(self.tree, "conf", "distributions")
  218. distributions = open(fn, "r").read() if os.path.exists(fn) else ""
  219. if "Codename: {0.release}\n".format(self) not in distributions:
  220. logging.info("%s %s", "Editing" if distributions else "Creating", fn)
  221. with open(fn, "w") as f:
  222. if distributions:
  223. f.write(distributions)
  224. f.write("\n")
  225. f.write(dedent("""\
  226. Origin: rpki.net
  227. Label: rpki.net {self.distribution} repository
  228. Codename: {self.release}
  229. Architectures: {architectures} source
  230. Components: main
  231. Description: rpki.net {Distribution} APT Repository
  232. SignWith: yes
  233. DebOverride: override.{self.release}
  234. DscOverride: override.{self.release}
  235. """.format(
  236. self = self,
  237. Distribution = self.distribution.capitalize(),
  238. architectures = " ".join(self.architectures))))
  239. fn = os.path.join(self.tree, "conf", "options")
  240. if not os.path.exists(fn):
  241. logging.info("Creating %s", fn)
  242. with open(fn, "w") as f:
  243. f.write(dedent("""\
  244. verbose
  245. ask-passphrase
  246. basedir .
  247. """))
  248. fn = os.path.join(self.tree, "conf", "override." + self.release)
  249. if not os.path.exists(fn):
  250. logging.info("Creating %s", fn)
  251. with open(fn, "w") as f:
  252. for pkg in self.backports:
  253. f.write(dedent("""\
  254. {pkg:<30} Priority optional
  255. {pkg:<30} Section python
  256. """.format(pkg = pkg)))
  257. f.write(dedent("""\
  258. rpki-ca Priority extra
  259. rpki-ca Section net
  260. rpki-rp Priority extra
  261. rpki-rp Section net
  262. """))
  263. fn = os.path.join(args.apt_tree, "rpki.{}.list".format(self.release))
  264. if not os.path.exists(fn):
  265. logging.info("Creating %s", fn)
  266. with open(fn, "w") as f:
  267. f.write(dedent("""\
  268. deb {self.apt_source}
  269. deb-src {self.apt_source}
  270. """.format(self = self)))
  271. # Load whatever releases the user specified
  272. for r in args.releases:
  273. Release(r, args.backports)
  274. # Do all the real work.
  275. Release.do_all_releases()
  276. # Push any tags created above to the public git repository.
  277. if upload:
  278. run("git", "push", "--tags", cwd = args.git_tree)
  279. # Upload results, maybe. We do this in two stages, to minimize the window
  280. # during which the uploaded repository might be in an inconsistent state.
  281. def rsync(*flags):
  282. cmd = ["rsync", "--archive", "--itemize-changes", "--rsh", "ssh"]
  283. cmd.extend(flags)
  284. cmd.append(args.apt_tree)
  285. cmd.append("rsync://{host}/{path}/".format(host = args.url_host,
  286. path = args.url_path.strip("/")))
  287. if upload:
  288. logging.info("Synching repository to %s with flags %s",
  289. cmd[-1], " ".join(flags))
  290. run(*cmd)
  291. else:
  292. logging.info("Would have synched repository to %s with flags %s",
  293. cmd[-1], " ".join(flags))
  294. rsync("--ignore-existing")
  295. rsync("--exclude", "HEADER.html",
  296. "--exclude", "HEADER.css",
  297. "--delete", "--delete-delay")
  298. logging.info("Done")