128b816f11e3cf864709f5028405aa04153cf18c
[~helmut/crossqa.git] / webapp.py
1 #!/usr/bin/python3
2
3 import datetime
4 import lzma
5
6 import apt_pkg
7 apt_pkg.init()
8 version_compare = apt_pkg.version_compare
9
10 import flask
11 import flask_sqlalchemy
12 import jinja2
13 import sqlalchemy
14 import werkzeug
15
16 app = flask.Flask("crossqa")
17 app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///db'
18 db = flask_sqlalchemy.SQLAlchemy(app)
19
20 src_template = """<!DOCTYPE html>
21 <html>
22  <head>
23  </head>
24  <body>
25   <title>{{ sourcepackage|e }} - Debian cross build</title>
26   <h1>{{ sourcepackage|e }}</h1>
27   <h3>Cross satisfiability</h3>
28   <table>
29    <tr>
30     <th>state</th>
31     <th>architectures</th>
32    </tr>
33    {%- set bootstrapdn = namespace(show=False) -%}
34    {%- set okarchs = depresult.pop(None, None) -%}
35    {%- for reason, archs in depresult.items()|sort -%}
36     {%- if not reason.startswith("skew ") -%}
37      {%- set bootstrapdn.show = True -%}
38     {%- endif -%}
39     <tr>
40      <td>{{ reason|e }}</td>
41      <td>{{ archs|arch_format }}</td>
42     </tr>
43    {%- endfor -%}
44    {%- if okarchs -%}
45     <tr>
46      <td>ok</td>
47      <td>{{ okarchs|arch_format }}</td>
48     </tr>
49    {%- endif -%}
50   </table>
51   <h5>See also</h5>
52   <ul>
53    {%- if bootstrapdn.show -%}
54     <li>
55      <a href="https://bootstrap.debian.net/cross_all/{{ sourcepackage|e }}.html">bootstrap.debian.net</a>
56     </li>
57    {%- endif -%}
58    <li>
59     <a href="https://qa.debian.org/dose/debcheck/cross_unstable_main_amd64/">debcheck</a>
60    </li>
61   </ul>
62   {%- if builds -%}
63    <h3>Cross builds</h3>
64    <table>
65     <tr>
66      <th>started</th>
67      <th>version</th>
68      <th>architecture</th>
69      <th>result</th>
70     </tr>
71     {%- for build in builds|sort(attribute='starttime', reverse=true) -%}
72      <tr>
73       <td>
74        <span title="{{ build.starttime|sqltimestamp }}">
75         {{- build.starttime|sqltimestamp|formatts -}}
76        </span>
77       </td>
78       <td>{{ build.version|e }}</td>
79       <td>{{ build.architecture|e }}</td>
80       <td>
81        <a href="{{ url_for("show_log", filename=build.filename[:-3]) }}">
82         {{- "ok" if build.success else "failed" -}}
83        </a>
84        <a href="{{ url_for("show_log", filename=build.filename) }}">xz</a>
85       </td>
86      </tr>
87     {%- endfor -%}
88    </table>
89   {%- endif -%}
90  </body>
91 </html>
92 """
93
94 @app.template_filter("sqltimestamp")
95 def sqltimestamp_filter(s):
96     return datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S.%f")
97
98 @app.template_filter("formatts")
99 def formatts_filter(ts):
100     assert isinstance(ts, datetime.datetime)
101     dt = datetime.datetime.utcnow() - ts
102     if dt < datetime.timedelta(seconds=1):
103         return "now"
104     if dt < datetime.timedelta(seconds=100):
105         return "%d s" % dt.seconds
106     if dt < datetime.timedelta(minutes=100):
107         return "%d m" % (dt.seconds // 60)
108     if dt < datetime.timedelta(days=1):
109         return "%d h" % (dt.seconds // (60 * 60))
110     return "%d d" % dt.days
111
112 @app.template_filter('arch_format')
113 @jinja2.contextfilter
114 def arch_format_filter(context, some_archs):
115     if context["architectures"] == some_archs:
116         return "any"
117     return ", ".join(sorted(some_archs))
118
119 def collect_depstate(conn, source):
120     version = None
121     depstate = None
122     query = sqlalchemy.text("""
123         SELECT version, architecture, satisfiable, reason
124             FROM depstate WHERE source = :source;""")
125     for row in conn.execute(query, source=source):
126         if version is None or version_compare(version, row.version) > 0:
127             version = row.version
128             depstate = {}
129         depstate[row.architecture] = None if row.satisfiable else row.reason
130     if version is None:
131         raise werkzeug.exceptions.NotFound()
132     depresult = {}
133     for arch, reason in depstate.items():
134         depresult.setdefault(reason, set()).add(arch)
135     return version, depresult
136
137 @app.route("/src/<source>")
138 def show_source(source):
139     context = dict(sourcepackage=source)
140     with db.engine.connect() as conn:
141         query = sqlalchemy.text("SELECT architecture FROM depcheck;")
142         context["architectures"] = set(row[0] for row in conn.execute(query))
143         context["version"], context["depresult"] = collect_depstate(conn,
144                                                                     source)
145         query = sqlalchemy.text("""
146             SELECT version, architecture, success, starttime, filename
147                 FROM builds WHERE source = :source;""")
148         context["builds"] = list(conn.execute(query, source=source))
149     return flask.render_template_string(src_template, **context)
150
151 @app.route("/build/<path:filename>")
152 def show_log(filename):
153     if filename.endswith(".xz"):
154         return flask.send_from_directory("logs", filename,
155                                          mimetype="application/octet-stream")
156     filename += ".xz"
157     return flask.send_file(lzma.open(flask.safe_join("logs", filename), "rb"),
158                            mimetype="text/plain")