rpki-pbuilder.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  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 = "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("git", "fetch", "--all", "--prune", cwd = args.git_tree)
  93. run("git", "pull", cwd = args.git_tree)
  94. source_version = subprocess.check_output((sys.executable, os.path.join(args.git_tree, "buildtools/make-version.py"),
  95. "--build-tag", "--stdout"), cwd = args.git_tree).strip()
  96. assert source_version.startswith("buildbot-")
  97. source_version = source_version[len("buildbot-"):].replace("-", ".")
  98. logging.info("Source version is %s", source_version)
  99. if not args.debug:
  100. try:
  101. run("git", "diff-index", "--quiet", "HEAD", cwd = args.git_tree)
  102. except subprocess.CalledProcessError:
  103. sys.exit("Sources don't look pristine, not building ({!r})".format(source_version))
  104. search_version = "_" + source_version + "~"
  105. dsc_dir = os.path.abspath(os.path.join(args.git_tree, ".."))
  106. if not os.path.isdir(args.apt_tree):
  107. logging.info("Creating %s", args.apt_tree)
  108. os.makedirs(args.apt_tree)
  109. fn = os.path.join(args.apt_tree, "apt-gpg-key.asc")
  110. if not os.path.exists(fn):
  111. logging.info("Creating %s", fn)
  112. run("gpg", "--export", "--armor", "--keyring", args.keyring, stdout = open(fn, "w"))
  113. class Release(object):
  114. architectures = dict(amd64 = "", i386 = "-i386")
  115. releases = []
  116. packages = {}
  117. def __init__(self, distribution_release, backports):
  118. self.distribution, self.release = distribution_release.split("/")
  119. self.backports = backports
  120. self.apt_source = "{scheme}://{host}/{path}/{distribution} {release} main".format(
  121. scheme = args.url_scheme,
  122. host = args.url_host,
  123. path = args.url_path.strip("/"),
  124. distribution = self.distribution,
  125. release = self.release)
  126. self.env = dict(os.environ)
  127. if backports:
  128. self.env.update(OTHERMIRROR = "deb " + self.apt_source)
  129. self.releases.append(self)
  130. @classmethod
  131. def do_all_releases(cls):
  132. for release in cls.releases:
  133. release.setup_reprepro()
  134. for release in cls.releases:
  135. release.list_repository()
  136. for release in cls.releases:
  137. for release.arch, release.tag in cls.architectures.iteritems():
  138. release.do_one_architecture()
  139. del release.arch, release.tag
  140. @staticmethod
  141. def repokey(release, architecture, package):
  142. return (release, architecture, package)
  143. def list_repository(self):
  144. cmd = ("reprepro", "list", self.release)
  145. logging.info("Running %s", " ".join(cmd))
  146. listing = subprocess.check_output(cmd, cwd = self.tree)
  147. for line in listing.replace(":", " ").replace("|", " ").splitlines():
  148. rel, comp, arch, pkg, ver = line.split()
  149. key = (rel, arch, pkg)
  150. assert key not in self.packages
  151. self.packages[key] = ver
  152. @property
  153. def deb_in_repository(self):
  154. ret = all(self.packages.get((self.release, self.arch, package)) == self.version
  155. for package in rpki_packages)
  156. #logging.info("Got %s looking for %r in %r", ret, self.version, self.packages)
  157. return ret
  158. @property
  159. def src_in_repository(self):
  160. ret = self.packages.get((self.release, "source", rpki_source_package)) == self.version
  161. #logging.info("Got %s looking for %r in %r", ret, self.version, self.packages)
  162. return ret
  163. @property
  164. def version(self):
  165. return source_version + "~" + self.release
  166. @property
  167. def dsc(self):
  168. return os.path.join(dsc_dir, "rpki_{}.dsc".format(self.version))
  169. @property
  170. def tree(self):
  171. return os.path.join(args.apt_tree, self.distribution, "")
  172. @property
  173. def basefile(self):
  174. return os.path.expanduser("~/pbuilder/{0.release}{0.tag}-base.tgz".format(self))
  175. @property
  176. def result(self):
  177. return os.path.expanduser("~/pbuilder/{0.release}{0.tag}_result".format(self))
  178. @property
  179. def changes(self):
  180. return os.path.join(self.result, "rpki_{0.version}_{0.arch}.changes".format(self))
  181. def do_one_architecture(self):
  182. logging.info("Running build for %s %s %s", self.distribution, self.release, self.arch)
  183. if not os.path.exists(self.dsc):
  184. logging.info("%s does not exist", self.dsc)
  185. for fn in os.listdir(dsc_dir):
  186. fn = os.path.join(dsc_dir, fn)
  187. if not os.path.isdir(fn) and search_version not in fn:
  188. logging.info("Removing %s", fn)
  189. os.unlink(fn)
  190. run("rm", "-rf", "debian", cwd = args.git_tree)
  191. logging.info("Building source package %s", self.version)
  192. run(sys.executable, "buildtools/build-debian-packages.py", "--version-suffix", self.release, cwd = args.git_tree)
  193. run("dpkg-buildpackage", "-S", "-us", "-uc", "-rfakeroot", cwd = args.git_tree)
  194. if not os.path.exists(self.basefile):
  195. logging.info("Creating build environment %s %s", self.release, self.arch)
  196. run("pbuilder-dist", self.release, self.arch, "create", env = self.env)
  197. elif time.time() > os.stat(self.basefile).st_mtime + args.update_build_after:
  198. logging.info("Updating build environment %s %s", self.release, self.arch)
  199. run("pbuilder-dist", self.release, self.arch, "update", env = self.env)
  200. if not self.deb_in_repository:
  201. for fn in os.listdir(self.result):
  202. fn = os.path.join(self.result, fn)
  203. logging.info("Removing %s", fn)
  204. os.unlink(fn)
  205. logging.info("Building binary packages %s %s %s", self.release, self.arch, self.version)
  206. run("pbuilder-dist", self.release, self.arch, "build", "--keyring", args.keyring, self.dsc, env = self.env)
  207. logging.info("Updating repository for %s %s %s", self.release, self.arch, self.version)
  208. run("reprepro", "--ignore=wrongdistribution", "include", self.release, self.changes, cwd = self.tree)
  209. if not self.src_in_repository:
  210. logging.info("Updating repository for %s source %s", self.release, self.version)
  211. run("reprepro", "--ignore=wrongdistribution", "includedsc", self.release, self.dsc, cwd = self.tree)
  212. def setup_reprepro(self):
  213. logging.info("Configuring reprepro for %s/%s", self.distribution, self.release)
  214. dn = os.path.join(self.tree, "conf")
  215. if not os.path.isdir(dn):
  216. logging.info("Creating %s", dn)
  217. os.makedirs(dn)
  218. fn = os.path.join(self.tree, "conf", "distributions")
  219. distributions = open(fn, "r").read() if os.path.exists(fn) else ""
  220. if "Codename: {0.release}\n".format(self) not in distributions:
  221. logging.info("%s %s", "Editing" if distributions else "Creating", fn)
  222. with open(fn, "w") as f:
  223. if distributions:
  224. f.write(distributions)
  225. f.write("\n")
  226. f.write(dedent("""\
  227. Origin: rpki.net
  228. Label: rpki.net {self.distribution} repository
  229. Codename: {self.release}
  230. Architectures: {architectures} source
  231. Components: main
  232. Description: rpki.net {Distribution} APT Repository
  233. SignWith: yes
  234. DebOverride: override.{self.release}
  235. DscOverride: override.{self.release}
  236. """.format(
  237. self = self,
  238. Distribution = self.distribution.capitalize(),
  239. architectures = " ".join(self.architectures))))
  240. fn = os.path.join(self.tree, "conf", "options")
  241. if not os.path.exists(fn):
  242. logging.info("Creating %s", fn)
  243. with open(fn, "w") as f:
  244. f.write(dedent("""\
  245. verbose
  246. ask-passphrase
  247. basedir .
  248. """))
  249. fn = os.path.join(self.tree, "conf", "override." + self.release)
  250. if not os.path.exists(fn):
  251. logging.info("Creating %s", fn)
  252. with open(fn, "w") as f:
  253. for pkg in self.backports:
  254. f.write(dedent("""\
  255. {pkg:<30} Priority optional
  256. {pkg:<30} Section python
  257. """.format(pkg = pkg)))
  258. f.write(dedent("""\
  259. rpki-ca Priority extra
  260. rpki-ca Section net
  261. rpki-rp Priority extra
  262. rpki-rp Section net
  263. """))
  264. fn = os.path.join(args.apt_tree, "rpki.{}.list".format(self.release))
  265. if not os.path.exists(fn):
  266. logging.info("Creating %s", fn)
  267. with open(fn, "w") as f:
  268. f.write(dedent("""\
  269. deb {self.apt_source}
  270. deb-src {self.apt_source}
  271. """.format(self = self)))
  272. # Load whatever releases the user specified
  273. for r in args.releases:
  274. Release(r, args.backports)
  275. # Do all the real work.
  276. Release.do_all_releases()
  277. # Upload results, maybe. We do this in two stages, to minimize the window
  278. # during which the uploaded repository might be in an inconsistent state.
  279. def rsync(*flags):
  280. cmd = ["rsync", "--archive", "--itemize-changes",
  281. "--rsh", "ssh -l {}".format(args.apt_user)]
  282. cmd.extend(flags)
  283. cmd.append(args.apt_tree)
  284. cmd.append("rsync://{host}/{path}/".format(host = args.url_host,
  285. path = args.url_path.strip("/")))
  286. if upload:
  287. logging.info("Synching repository to %s with flags %s",
  288. cmd[-1], " ".join(flags))
  289. run(*cmd)
  290. else:
  291. logging.info("Would have synched repository to %s with flags %",
  292. cmd[-1], " ".join(flags))
  293. rsync("--ignore-existing")
  294. rsync("--exclude", "HEADER.html",
  295. "--exclude", "HEADER.css",
  296. "--delete", "--delete-delay")
  297. logging.info("Done")