blob: 3ce209cdd77b0bd767f2313e4201ca8304485ec4 [file] [log] [blame]
Zack Williams712caf62020-04-28 13:37:41 -07001#!/usr/bin/env python3
2
3# SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org>
4# SPDX-License-Identifier: Apache-2.0
5
6from __future__ import absolute_import
7
8import argparse
9import datetime
10import jinja2
11import json
12import logging
13import os
14
15# create shared logger
16logging.basicConfig()
17logger = logging.getLogger("sjsgsr")
18
19# script starttime, used for timeboxing
20starttime = datetime.datetime.now(datetime.timezone.utc)
21startdate = datetime.datetime(
22 starttime.year, starttime.month, starttime.day, tzinfo=datetime.timezone.utc
23)
24
25
26def parse_sitebuilder_args():
27 """
28 parse CLI arguments
29 """
30
31 parser = argparse.ArgumentParser(description="Jenkins job results site renderer")
32
33 def readable_dir(path):
34 if os.path.isdir(path) and os.access(path, os.R_OK):
35 return path
36 raise argparse.ArgumentTypeError("%s is not a directory or unreadable" % path)
37
38 # Flags
39 parser.add_argument(
40 "--product_dir",
41 default="products",
42 type=readable_dir,
43 help="Directory containing product JSON created by buildcollector.py",
44 )
45 parser.add_argument(
46 "--template_dir",
47 default="templates",
48 type=readable_dir,
49 help="Directory with Jinja2 templates",
50 )
51 parser.add_argument(
52 "--site_dir",
53 default="site",
54 type=readable_dir,
55 help="Directory to write the static site into",
56 )
57 parser.add_argument(
58 "--debug", action="store_true", help="Print additional debugging information"
59 )
60
61 return parser.parse_args()
62
63
64def jinja_env(template_dir):
65 """
66 Returns a Jinja2 enviroment loading files from template_dir
67 """
68
69 env = jinja2.Environment(
70 loader=jinja2.FileSystemLoader(template_dir),
71 autoescape=jinja2.select_autoescape(["html"]),
72 undefined=jinja2.StrictUndefined,
73 trim_blocks=True,
74 lstrip_blocks=True,
75 )
76
77 # Jinja2 filters
78 def tsdatetime(value, fmt="%Y-%m-%d %H:%M %Z"):
79 # timestamps given ms precision, divide by 1000
80 dateval = datetime.datetime.fromtimestamp(
81 value // 1000, tz=datetime.timezone.utc
82 )
83 return dateval.strftime(fmt)
84
85 def tsdate(value, fmt="%Y-%m-%d"):
86 # timestamps given ms precision, divide by 1000
87 dateval = datetime.datetime.fromtimestamp(
88 value // 1000, tz=datetime.timezone.utc
89 )
90 return dateval.strftime(fmt)
91
92 def timebox(value, boxsize=24, count=7):
93 """
94 Given a list of builds, groups in them into within a sequential range
95 Boxsize is given in hours
96 Count is number of boxes to return
97 Time starts from now()
98 """
99 retbox = []
100
101 nowms = startdate.timestamp() * 1000 # ms precision datetime
102
103 hourms = 60 * 60 * 1000 # ms in an hour
104
105 for box in range(-1, count - 1):
106
107 startms = nowms - (box * hourms * boxsize)
108 endms = nowms - ((box + 1) * hourms * boxsize)
109 timebox_builds = 0
110 success_count = 0
111
112 builds = []
113
114 for bdata in value:
115 # loops multiple times over entire list of builds, could be
116 # optimized
117
118 bt = int(bdata["timestamp"])
119
120 if startms > bt > endms:
121 timebox_builds += 1
122
123 builds.append(bdata)
124
125 if bdata["result"] == "SUCCESS":
126 success_count += 1
127
128 # determine overall status for the timebox
129 if timebox_builds == 0:
130 status = "NONE"
131 elif timebox_builds == success_count:
132 status = "SUCCESS"
133 elif success_count == 0:
134 status = "FAILURE"
135 else: # timebox_builds > success_count
136 status = "UNSTABLE"
137
138 retbox.append(
139 {
140 "result": status,
141 "box_start": int(endms),
142 "outcome": "%d of %d" % (success_count, timebox_builds),
143 "builds": builds,
144 }
145 )
146
147 return retbox
148
149 env.filters["tsdatetime"] = tsdatetime
150 env.filters["tsdate"] = tsdate
151 env.filters["timebox"] = timebox
152
153 return env
154
155
156def clean_name(name):
157 """
158 Clean up a name string. Currently only replaces spaces with underscores
159 """
160 return name.replace(" ", "_")
161
162
163def render_to_file(j2env, context, template_name, path):
164 """
165 Render out a template to file
166 """
167 parent_dir = os.path.dirname(path)
168 os.makedirs(parent_dir, exist_ok=True)
169
170 template = j2env.get_template(template_name)
171
172 with open(path, "w") as outfile:
173 outfile.write(template.render(context))
174
175
176def json_file_load(path):
177 """
178 Get data from local file, return data as a dict
179 """
180
181 with open(path) as jf:
182 try:
183 data = json.loads(jf.read())
184 except json.decoder.JSONDecodeError:
185 logger.exception("Unable to decode JSON from file: '%s'", path)
186
187 return data
188
189
190# main function that calls other functions
191if __name__ == "__main__":
192
193 args = parse_sitebuilder_args()
194
195 # only print log messages if debugging
196 if args.debug:
197 logger.setLevel(logging.DEBUG)
198 else:
199 logger.setLevel(logging.CRITICAL)
200
201 j2env = jinja_env(args.template_dir)
202
203 buildtime = starttime.strftime("%Y-%m-%d %H:%M %Z")
204
205 # init list of projects
206 projects = {}
207
Zack Williams02047882020-10-28 11:04:07 -0700208 # list of projects
209 projdirs = os.listdir(args.product_dir)
Zack Williams712caf62020-04-28 13:37:41 -0700210
Zack Williams02047882020-10-28 11:04:07 -0700211 for projdir in projdirs:
212 prodfiles = os.listdir("%s/%s" % (args.product_dir, projdir))
Zack Williams712caf62020-04-28 13:37:41 -0700213
Zack Williams02047882020-10-28 11:04:07 -0700214 for prodfile in prodfiles:
Zack Williams712caf62020-04-28 13:37:41 -0700215
Zack Williams02047882020-10-28 11:04:07 -0700216 # load product, and set buildtime
217 logger.debug("loading file file: '%s'", prodfile)
218 product = json_file_load("%s/%s/%s" % (args.product_dir, projdir, prodfile))
219 product.update({"buildtime": buildtime})
Zack Williams712caf62020-04-28 13:37:41 -0700220
Zack Williams02047882020-10-28 11:04:07 -0700221 projname = product["onf_project"]
Zack Williams712caf62020-04-28 13:37:41 -0700222
Zack Williams02047882020-10-28 11:04:07 -0700223 # build product filename, write out template
224 site_prod_filename = "%s/%s/%s/index.html" % (
225 args.site_dir,
226 projname,
227 clean_name(product["product_name"]),
228 )
229 render_to_file(j2env, product, "product.html", site_prod_filename)
230
231 # product to project list
232 if projname not in projects:
233 projects[projname] = [product]
234 else:
235 projects[projname].append(product)
Zack Williams712caf62020-04-28 13:37:41 -0700236
237 # list of projects
238 for projname in sorted(projects.keys()):
239
240 proj_filename = "%s/%s/index.html" % (args.site_dir, projname)
241
242 products = projects[projname]
243
244 project_context = {
245 "buildtime": buildtime,
246 "project_name": projname,
247 "products": products,
248 }
249
250 render_to_file(j2env, project_context, "project.html", proj_filename)
251
252 # render index file
253 index_filename = "%s/index.html" % args.site_dir
254
255 index_context = {
256 "buildtime": buildtime,
257 "projects": projects,
258 }
259
260 render_to_file(j2env, index_context, "index.html", index_filename)