e32a8afaef7b09ca2847aa7d3607e6c20408583b
[~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 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
19 db = flask_sqlalchemy.SQLAlchemy(app)
20
21 src_template = """<!DOCTYPE html>
22 <html>
23  <head>
24   <title>{{ sourcepackage|e }} - Debian cross build</title>
25   <style>
26 tr.dep.bad td:nth-child(1) {
27     background-color: #faa;
28 }
29 tr.dep.tempbad td:nth-child(1) {
30     background-color: #ffa;
31 }
32 tr.dep.good td:nth-child(1) {
33     background-color: #afa;
34 }
35 tr.build.bad td:nth-child(4) {
36     background-color: #faa;
37 }
38 tr.build.tempbad td:nth-child(4) {
39     background-color: #ffa;
40 }
41 tr.build.good td:nth-child(4) {
42     background-color: #afa;
43 }
44 th {
45     padding-left: 1em;
46     padding-right: 1em;
47 }
48 td {
49     padding-left: 1px;
50     padding-right: 1em;
51 }
52 td:last-child {
53     padding-right: 1px;
54 }
55   </style>
56  </head>
57  <body>
58   <header>
59    <h1>
60     <a href="https://tracker.debian.org/pkg/{{ sourcepackage|e }}">
61      {{- sourcepackage|e -}}
62     </a>
63    </h1>
64   </header>
65   <section>
66    <h3>Cross build dependency satisfiability</h3>
67    <table>
68     <thead>
69      <tr>
70       <th>state</th>
71       <th>architectures</th>
72      </tr>
73     </thead>
74     <tbody>
75      {%- set okarchs = depresult.pop(None, None) -%}
76      {%- for reason, archs in depresult.items()|sort -%}
77       <tr class="dep {{ "tempbad" if reason.startswith("skew") else "bad" }}">
78        <td>{{ reason|e }}</td>
79        <td>{{ archs|arch_format }}</td>
80       </tr>
81      {%- endfor -%}
82      {%- if okarchs -%}
83       <tr class="dep good">
84        <td>ok</td>
85        <td>{{ okarchs|arch_format }}</td>
86       </tr>
87      {%- endif -%}
88     </tbody>
89    </table>
90    <h5>See also</h5>
91    <ul>
92     {%- if show_bootstrapdn -%}
93      <li>
94       <a href="https://bootstrap.debian.net/cross_all/{{ sourcepackage|e }}.html">bootstrap.debian.net</a>
95      </li>
96     {%- endif -%}
97     <li>
98      <a href="https://qa.debian.org/dose/debcheck/cross_unstable_main_amd64/">debcheck</a>
99     </li>
100    </ul>
101   </section>
102   {%- if builds -%}
103    <section>
104     <h3>Cross builds</h3>
105     <table>
106      <thead>
107       <tr>
108        <th>started</th>
109        <th>version</th>
110        <th>architecture</th>
111        <th>result log</th>
112       </tr>
113      </thead>
114      <tbody>
115       {%- for build in builds|sort(attribute='starttime', reverse=true) -%}
116        <tr class="build {{ "good" if build.success else "bad" }}">
117         <td>
118          {{- build.starttime|sqltimestamp|formatts -}}
119         </td>
120         <td>{{ build.version|e }}</td>
121         <td>{{ build.architecture|e }}</td>
122         <td>
123          <a href="{{ url_for("show_log", filename=build.filename[:-3]) }}">
124           {{- "ok" if build.success else "failed" -}}
125          </a>
126          <a href="{{ url_for("show_log", filename=build.filename) }}">xz</a>
127         </td>
128        </tr>
129       {%- endfor -%}
130      </tbody>
131     </table>
132    </section>
133   {%- endif -%}
134  </body>
135 </html>
136 """
137
138 @app.template_filter("sqltimestamp")
139 def sqltimestamp_filter(s):
140     strptime = datetime.datetime.strptime
141     try:
142         return strptime(s, "%Y-%m-%d %H:%M:%S.%f").replace(microsecond=0)
143     except ValueError:
144         return strptime(s, "%Y-%m-%d %H:%M:%S")
145
146
147 def formatts(ts):
148     assert isinstance(ts, datetime.datetime)
149     dt = datetime.datetime.utcnow() - ts
150     if dt < datetime.timedelta(seconds=1):
151         return "now"
152     if dt < datetime.timedelta(seconds=100):
153         return "%d s" % dt.seconds
154     if dt < datetime.timedelta(minutes=100):
155         return "%d m" % (dt.seconds // 60)
156     if dt < datetime.timedelta(days=1):
157         return "%d h" % (dt.seconds // (60 * 60))
158     return "%d d" % dt.days
159
160
161 @app.template_filter("formatts")
162 def formatts_filter(ts):
163     return jinja2.Markup('<time title="%s" datetime="%s">%s</time>' %
164                          (ts, ts, formatts(ts)))
165
166 @app.template_filter('arch_format')
167 @jinja2.contextfilter
168 def arch_format_filter(context, some_archs):
169     if context["architectures"] == some_archs:
170         return "any"
171     return ", ".join(sorted(some_archs))
172
173 def collect_depstate(conn, source):
174     version = None
175     depstate = None
176     query = sqlalchemy.text("""
177         SELECT version, architecture, satisfiable, reason
178             FROM depstate WHERE source = :source;""")
179     for row in conn.execute(query, source=source):
180         if version is None or version_compare(version, row.version) > 0:
181             version = row.version
182             depstate = {}
183         depstate[row.architecture] = None if row.satisfiable else row.reason
184     if version is None:
185         raise werkzeug.exceptions.NotFound()
186     depresult = {}
187     for arch, reason in depstate.items():
188         depresult.setdefault(reason, set()).add(arch)
189     return version, depresult
190
191 @app.route("/src/<source>")
192 def show_source(source):
193     context = dict(sourcepackage=source)
194     with db.engine.connect() as conn:
195         query = sqlalchemy.text("SELECT architecture FROM depcheck;")
196         context["architectures"] = set(row[0] for row in conn.execute(query))
197         context["version"], context["depresult"] = collect_depstate(conn,
198                                                                     source)
199         query = sqlalchemy.text("""
200             SELECT version, architecture, success, starttime, filename
201                 FROM builds WHERE source = :source;""")
202         context["builds"] = list(conn.execute(query, source=source))
203         context["show_bootstrapdn"] = \
204                 any(reason and not reason.startswith("skew ")
205                     for reason in context["depresult"].keys())
206     return flask.render_template_string(src_template, **context)
207
208 @app.route("/build/<path:filename>")
209 def show_log(filename):
210     if filename.endswith(".xz"):
211         return flask.send_from_directory("logs", filename,
212                                          mimetype="application/octet-stream")
213     filename = flask.safe_join("logs", filename + ".xz")
214     try:
215         return flask.send_file(lzma.open(filename, "rb"),
216                                mimetype="text/plain")
217     except FileNotFoundError:
218         raise werkzeug.exceptions.NotFound()