Merge lp:~jelmer/lptools/recipe-status into lp:lptools

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: 29
Merged at revision: 33
Proposed branch: lp:~jelmer/lptools/recipe-status
Merge into: lp:lptools
Diff against target: 346 lines (+305/-0)
5 files modified
bin/lp-recipe-status (+210/-0)
lptools/config.py (+13/-0)
setup.py (+2/-0)
templates/recipe-status.css (+38/-0)
templates/recipe-status.html (+42/-0)
To merge this branch: bzr merge lp:~jelmer/lptools/recipe-status
Reviewer Review Type Date Requested Status
Martin Pool Approve
Review via email: mp+73135@code.launchpad.net

Description of the change

Import "recipe-status".

This script generates a text or html overview of the status of all recipes owned by a particular person or team.

For example output, see http://people.canonical.com/~jelmer/recipe-status/bzr.html

I had to add a new utility function data_dir, which tries to find the templates for recipe-status. It's a bit hackish, so I'd love to hear suggestions for better ways to do it.

To post a comment you must log in.
lp:~jelmer/lptools/recipe-status updated
28. By Jelmer Vernooij

Fix some long lines.

29. By Jelmer Vernooij

Rename to lp-recipe-status, exclude distroseries that have been disabled.

Revision history for this message
Martin Pool (mbp) wrote :

The main function is not the prettiest, but overall it looks decent and worth having in here. You ought to mention it in the readme, and maybe on the launchpad blog.

review: Approve
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

I've factored out the two main bits of the main function so it should be a bit more readable now, and added a bunch of docstrings.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'bin/lp-recipe-status'
2--- bin/lp-recipe-status 1970-01-01 00:00:00 +0000
3+++ bin/lp-recipe-status 2011-08-30 14:19:50 +0000
4@@ -0,0 +1,210 @@
5+#!/usr/bin/python
6+
7+# Copyright (C) 2011 Jelmer Vernooij <jelmer@samba.org>
8+#
9+# ##################################################################
10+#
11+# This program is free software; you can redistribute it and/or
12+# modify it under the terms of the GNU General Public License
13+# as published by the Free Software Foundation; version 3.
14+#
15+# This program is distributed in the hope that it will be useful,
16+# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+# GNU General Public License for more details.
19+#
20+# See file /usr/share/common-licenses/GPL-3 for more details.
21+#
22+# ##################################################################
23+#
24+# vi: expandtab:sts=4
25+
26+"""Show the status of the recipes owned by a particular user.
27+"""
28+
29+from cStringIO import StringIO
30+import gzip
31+import optparse
32+import os
33+import re
34+import sys
35+import urllib
36+
37+from lptools import config
38+
39+try:
40+ import tdb
41+except ImportError:
42+ cache = {}
43+else:
44+ cache = tdb.Tdb("recipe-status-cache.tdb", 1000, tdb.DEFAULT,
45+ os.O_RDWR|os.O_CREAT)
46+
47+def gather_per_distroseries_source_builds(recipe):
48+ last_per_distroseries = {}
49+ for build in recipe.completed_builds:
50+ if build.distro_series.self_link not in recipe.distroseries:
51+ # Skip distro series that are no longer being build
52+ continue
53+ distro_series_name = build.distro_series.name
54+ previous_build = last_per_distroseries.get(distro_series_name)
55+ if previous_build is None or previous_build.datecreated < build.datecreated:
56+ last_per_distroseries[distro_series_name] = build
57+ return last_per_distroseries
58+
59+
60+def build_failure_link(build):
61+ if build.buildstate == "Failed to upload":
62+ return build.upload_log_url
63+ elif build.buildstate in ("Failed to build", "Dependency wait", "Chroot problem"):
64+ return build.build_log_url
65+ else:
66+ return None
67+
68+version_matcher = re.compile("^dpkg-buildpackage: source version (.*)$")
69+source_name_matcher = re.compile("^dpkg-buildpackage: source package (.*)$")
70+
71+def source_build_find_version(source_build):
72+ cached_version = cache.get("version/%s" % str(source_build.self_link))
73+ if cached_version:
74+ return tuple(cached_version.split(" "))
75+ # FIXME: Find a more efficient way to retrieve the package/version that was built
76+ build_log_gz = urllib.urlopen(source_build.build_log_url)
77+ build_log = gzip.GzipFile(fileobj=StringIO(build_log_gz.read()))
78+ version = None
79+ source_name = None
80+ for l in build_log.readlines():
81+ m = version_matcher.match(l)
82+ if m:
83+ version = m.group(1)
84+ m = source_name_matcher.match(l)
85+ if m:
86+ source_name = m.group(1)
87+ if not source_name:
88+ raise Exception("unable to find source name in %s" %
89+ source_build.build_log_url)
90+ if not version:
91+ raise Exception("unable to find version in %s" %
92+ source_build.build_log_url)
93+ cache["version/%s" % str(source_build.self_link)] = "%s %s" % (
94+ source_name, version)
95+ return (source_name, version)
96+
97+
98+def find_binary_builds(recipe, source_builds):
99+ ret = {}
100+ for source_build in source_builds:
101+ archive = source_build.archive
102+ (source_name, version) = source_build_find_version(source_build)
103+ sources = archive.getPublishedSources(
104+ distro_series=source_build.distro_series,
105+ exact_match=True, pocket="Release", source_name=source_name,
106+ version=version)
107+ assert len(sources) == 1
108+ source = sources[0]
109+ ret[source_build.distro_series.name] = source.getBuilds()
110+ return ret
111+
112+
113+def build_failure_summary(build):
114+ # FIXME: Perhaps parse the relevant logs and extract a summary line?
115+ return build.buildstate
116+
117+def build_class(build):
118+ return {
119+ "Failed to build": "failed-to-build",
120+ "Failed to upload": "failed-to-upload",
121+ "Dependency wait": "dependency-wait",
122+ "Chroot problem": "chroot-problem",
123+ "Uploading build": "uploading-build",
124+ "Currently building": "currently-building",
125+ "Build for superseded Source": "superseded-source",
126+ "Successfully built": "successfully-built",
127+ "Needs building": "needs-building",
128+ }[build.buildstate]
129+
130+def filter_source_builds(builds):
131+ sp_success = set()
132+ sp_failures = set()
133+ for build in builds:
134+ if build.buildstate == "Successfully built":
135+ sp_success.add(build)
136+ else:
137+ sp_failures.add(build)
138+ return (sp_success, sp_failures)
139+
140+
141+def main(argv):
142+ parser = optparse.OptionParser('%prog [options] PERSON\n\n'
143+ ' PERSON is the launchpad person whose recipes to check')
144+ parser.add_option("--html", help="Generate HTML", action="store_true")
145+ opts, args = parser.parse_args()
146+ if len(args) != 1:
147+ parser.print_usage()
148+ return 1
149+ person = args[0]
150+ launchpad = config.get_launchpad("recipe-status")
151+ person = launchpad.people[person]
152+
153+ if opts.html:
154+ from chameleon.zpt.loader import TemplateLoader
155+ tl = TemplateLoader(os.path.join(config.data_dir(), "templates"))
156+ relevant_distroseries = set()
157+ source_builds = {}
158+ binary_builds = {}
159+ all_binary_builds_ok = {}
160+ for recipe in person.recipes:
161+ sys.stderr.write("Processing recipe %s\n" % recipe.name)
162+ last_per_distroseries = gather_per_distroseries_source_builds(recipe)
163+ source_builds[recipe.name] = last_per_distroseries
164+ relevant_distroseries.update(set(last_per_distroseries))
165+ (sp_success, sp_failures) = filter_source_builds(last_per_distroseries.values())
166+ binary_builds[recipe.name] = find_binary_builds(recipe, sp_success)
167+ all_binary_builds_ok[recipe.name] = {}
168+ for distroseries, recipe_binary_builds in binary_builds[recipe.name].iteritems():
169+ all_binary_builds_ok[recipe.name][distroseries] = all(
170+ [bb.buildstate == "Successfully built" for bb in recipe_binary_builds])
171+ relevant_distroseries = list(relevant_distroseries)
172+ relevant_distroseries.sort()
173+ page = tl.load("recipe-status.html")
174+ print page.render(person=person,
175+ relevant_distroseries=relevant_distroseries,
176+ recipes=person.recipes, source_builds=source_builds,
177+ build_failure_summary=build_failure_summary,
178+ build_failure_link=build_failure_link,
179+ binary_builds=binary_builds,
180+ ubuntu=launchpad.distributions["ubuntu"],
181+ build_class=build_class,
182+ all_binary_builds_ok=all_binary_builds_ok)
183+ else: # text
184+ for recipe in person.recipes:
185+ last_per_distroseries = gather_per_distroseries_source_builds(recipe)
186+ (sp_success, sp_failures) = filter_source_builds(last_per_distroseries.values())
187+ sp_success_distroseries = [build.distro_series.name for build in sp_success]
188+ if sp_failures:
189+ print "%s source build failures (%s successful):" % (recipe.name, ", ".join(sp_success_distroseries))
190+ for failed_build in sp_failures:
191+ url = build_failure_link(failed_build)
192+ sys.stdout.write(" %s(%s)" % (failed_build.distro_series.name, failed_build.buildstate))
193+ if url:
194+ sys.stdout.write(": %s" % url)
195+ sys.stdout.write("\n")
196+ elif sp_success:
197+ print "%s source built successfully on %s" % (recipe.name, ", ".join(sp_success_distroseries))
198+ else:
199+ print "%s never built" % recipe.name
200+ binary_builds = find_binary_builds(recipe, sp_success)
201+ for source_build in sp_success:
202+ for binary_build in binary_builds[source_build]:
203+ if binary_build.buildstate != "Successfully built":
204+ url = build_failure_link(binary_build)
205+ sys.stdout.write(" %s,%s(%s)" %
206+ (binary_build.distro_series.name,
207+ binary_build.arch_tag,
208+ binary_build.buildstate))
209+ if url:
210+ sys.stdout.write(": %s" % url)
211+ sys.stdout.write("\n")
212+
213+if __name__ == '__main__':
214+ sys.exit(main(sys.argv))
215
216=== modified file 'lptools/config.py'
217--- lptools/config.py 2011-08-29 14:21:32 +0000
218+++ lptools/config.py 2011-08-30 14:19:50 +0000
219@@ -19,6 +19,7 @@
220 """Configuration glue for lptools."""
221
222 __all__ = [
223+ "data_dir",
224 "ensure_dir",
225 "get_launchpad",
226 ]
227@@ -44,3 +45,15 @@
228 cachedirs.
229 """
230 return Launchpad.login_with("lptools-%s" % appname, "production")
231+
232+
233+def data_dir():
234+ """Return the arch-independent data directory.
235+ """
236+ # Running from source directory?
237+ ret = os.path.join(os.path.dirname(__file__), "..")
238+ if os.path.exists(os.path.join(ret, "templates")):
239+ return ret
240+ else:
241+ return os.path.abspath(os.path.join(os.path.dirname(__file__),
242+ "../../../../share/lptools"))
243
244=== modified file 'setup.py'
245--- setup.py 2010-04-23 15:16:45 +0000
246+++ setup.py 2011-08-30 14:19:50 +0000
247@@ -16,6 +16,8 @@
248 description='A collection of tools for developers who use launchpad',
249 long_description=description,
250 py_modules=[],
251+ data_files=[('share/lptools/templates', ['templates/recipe-status.css',
252+ 'templates/recipe-status.html'])],
253 packages=['lptools'],
254 scripts=glob('bin/*'),
255 classifiers = [
256
257=== added directory 'templates'
258=== added file 'templates/recipe-status.css'
259--- templates/recipe-status.css 1970-01-01 00:00:00 +0000
260+++ templates/recipe-status.css 2011-08-30 14:19:50 +0000
261@@ -0,0 +1,38 @@
262+body {
263+ background: url(background.png) repeat;
264+ color: #222222;
265+ font-family: "UbuntuBeta", Ubuntu, "Bitstream Vera Sans", "DejaVu Sans", Tahoma, sans-serif;
266+ font-size: 12px;
267+}
268+
269+a {
270+ text-decoration: none;
271+}
272+
273+a:hover {
274+ text-decoration: underline;
275+}
276+
277+a:visited {
278+ color: blue;
279+}
280+
281+.failed-to-upload, .failed-to-build {
282+ background: red;
283+}
284+
285+.chroot-problem, .dependency-wait {
286+ background: orange;
287+}
288+
289+.not-available {
290+ background: gray;
291+}
292+
293+.successfully-built {
294+ background: chartreuse;
295+}
296+
297+.partial-success {
298+ background: yellow;
299+}
300
301=== added file 'templates/recipe-status.html'
302--- templates/recipe-status.html 1970-01-01 00:00:00 +0000
303+++ templates/recipe-status.html 2011-08-30 14:19:50 +0000
304@@ -0,0 +1,42 @@
305+<html>
306+ <head>
307+ <title>Recipe status for <omit tal:replace="person.name"/></title>
308+ <link rel="stylesheet" type="text/css" href="recipe-status.css" />
309+ </head>
310+
311+ <body>
312+ <table class="recipes">
313+ <tr>
314+ <th>Recipe</th>
315+ <th tal:repeat="distroseries relevant_distroseries" class="recipe-column">
316+ <a tal:attributes="href ubuntu.getSeries(name_or_version=distroseries).web_link" tal:content="distroseries"/>
317+ </th>
318+ </tr>
319+ <tr tal:repeat="recipe recipes" class="recipe">
320+ <td class="recipe"><a tal:attributes="href recipe.web_link" tal:content="recipe.name"/></td>
321+ <distroseries tal:omit-tag="" tal:repeat="distroseries relevant_distroseries">
322+ <source-build tal:omit-tag="" tal:define="global source_build source_builds[recipe.name].get(distroseries)"/>
323+ <td tal:condition="not source_build" class="not-available">N/A</td>
324+ <td tal:condition="source_build and source_build.buildstate != 'Successfully built'" tal:attributes="class build_class(source_build)">
325+ <a tal:attributes="href build_failure_link(source_build)"
326+ tal:omit-tag="not build_failure_link(source_build)"
327+ tal:content="'Source:'+build_failure_summary(source_build)"/>
328+ </td>
329+ <source-build-success tal:omit-tag="" tal:condition="source_build and source_build.buildstate == 'Successfully built'">
330+ <td tal:condition="all_binary_builds_ok[recipe.name].get(distroseries)" class="successfully-built">OK</td>
331+ <td tal:condition="not all_binary_builds_ok[recipe.name].get(distroseries)" class="partial-success">
332+ <binary-build tal:omit-tag="" tal:repeat="binary_build binary_builds[recipe.name][distroseries]">
333+ <div tal:attributes="class build_class(binary_build)">
334+ <a tal:attributes="href build_failure_link(binary_build)"
335+ tal:omit-tag="not build_failure_link(binary_build)"
336+ tal:content="binary_build.arch_tag+':'+build_failure_summary(binary_build)"/>
337+ </div>
338+ </binary-build>
339+ </td>
340+ </source-build-success>
341+ </distroseries>
342+ </tr>
343+ </table>
344+ <p>Generated using recipe-status. For the source see <a href="https://code.launchpad.net/lptools">lptools</a>.</p>
345+ </body>
346+</html>

Subscribers

People subscribed via source and target branches