b9558cdfcb175c7135bec2d6c10262c9ea8573c0
[~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  </head>
25  <body>
26   <title>{{ sourcepackage|e }} - Debian cross build</title>
27   <h1>{{ sourcepackage|e }}</h1>
28   <h3>Cross satisfiability</h3>
29   <table>
30    <tr>
31     <th>state</th>
32     <th>architectures</th>
33    </tr>
34    {%- set okarchs = depresult.pop(None, None) -%}
35    {%- for reason, archs in depresult.items()|sort -%}
36     <tr>
37      <td>{{ reason|e }}</td>
38      <td>{{ archs|arch_format }}</td>
39     </tr>
40    {%- endfor -%}
41    {%- if okarchs -%}
42     <tr>
43      <td>ok</td>
44      <td>{{ okarchs|arch_format }}</td>
45     </tr>
46    {%- endif -%}
47   </table>
48   <h5>See also</h5>
49   <ul>
50    {%- if show_bootstrapdn -%}
51     <li>
52      <a href="https://bootstrap.debian.net/cross_all/{{ sourcepackage|e }}.html">bootstrap.debian.net</a>
53     </li>
54    {%- endif -%}
55    <li>
56     <a href="https://qa.debian.org/dose/debcheck/cross_unstable_main_amd64/">debcheck</a>
57    </li>
58   </ul>
59   {%- if builds -%}
60    <h3>Cross builds</h3>
61    <table>
62     <tr>
63      <th>started</th>
64      <th>version</th>
65      <th>architecture</th>
66      <th>result</th>
67     </tr>
68     {%- for build in builds|sort(attribute='starttime', reverse=true) -%}
69      <tr>
70       <td>
71        <span title="{{ build.starttime|sqltimestamp }}">
72         {{- build.starttime|sqltimestamp|formatts -}}
73        </span>
74       </td>
75       <td>{{ build.version|e }}</td>
76       <td>{{ build.architecture|e }}</td>
77       <td>
78        <a href="{{ url_for("show_log", filename=build.filename[:-3]) }}">
79         {{- "ok" if build.success else "failed" -}}
80        </a>
81        <a href="{{ url_for("show_log", filename=build.filename) }}">xz</a>
82       </td>
83      </tr>
84     {%- endfor -%}
85    </table>
86   {%- endif -%}
87  </body>
88 </html>
89 """
90
91 @app.template_filter("sqltimestamp")
92 def sqltimestamp_filter(s):
93     return datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S.%f")
94
95 @app.template_filter("formatts")
96 def formatts_filter(ts):
97     assert isinstance(ts, datetime.datetime)
98     dt = datetime.datetime.utcnow() - ts
99     if dt < datetime.timedelta(seconds=1):
100         return "now"
101     if dt < datetime.timedelta(seconds=100):
102         return "%d s" % dt.seconds
103     if dt < datetime.timedelta(minutes=100):
104         return "%d m" % (dt.seconds // 60)
105     if dt < datetime.timedelta(days=1):
106         return "%d h" % (dt.seconds // (60 * 60))
107     return "%d d" % dt.days
108
109 @app.template_filter('arch_format')
110 @jinja2.contextfilter
111 def arch_format_filter(context, some_archs):
112     if context["architectures"] == some_archs:
113         return "any"
114     return ", ".join(sorted(some_archs))
115
116 def collect_depstate(conn, source):
117     version = None
118     depstate = None
119     query = sqlalchemy.text("""
120         SELECT version, architecture, satisfiable, reason
121             FROM depstate WHERE source = :source;""")
122     for row in conn.execute(query, source=source):
123         if version is None or version_compare(version, row.version) > 0:
124             version = row.version
125             depstate = {}
126         depstate[row.architecture] = None if row.satisfiable else row.reason
127     if version is None:
128         raise werkzeug.exceptions.NotFound()
129     depresult = {}
130     for arch, reason in depstate.items():
131         depresult.setdefault(reason, set()).add(arch)
132     return version, depresult
133
134 @app.route("/src/<source>")
135 def show_source(source):
136     context = dict(sourcepackage=source)
137     with db.engine.connect() as conn:
138         query = sqlalchemy.text("SELECT architecture FROM depcheck;")
139         context["architectures"] = set(row[0] for row in conn.execute(query))
140         context["version"], context["depresult"] = collect_depstate(conn,
141                                                                     source)
142         query = sqlalchemy.text("""
143             SELECT version, architecture, success, starttime, filename
144                 FROM builds WHERE source = :source;""")
145         context["builds"] = list(conn.execute(query, source=source))
146         context["show_bootstrapdn"] = \
147                 any(reason and not reason.startswith("skew ")
148                     for reason in context["depresult"].keys())
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")