Zack Williams | 712caf6 | 2020-04-28 13:37:41 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | # SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org> |
| 4 | # SPDX-License-Identifier: Apache-2.0 |
| 5 | |
| 6 | from __future__ import absolute_import |
| 7 | |
| 8 | import argparse |
| 9 | import datetime |
| 10 | import jinja2 |
| 11 | import json |
| 12 | import logging |
| 13 | import os |
| 14 | |
| 15 | # create shared logger |
| 16 | logging.basicConfig() |
| 17 | logger = logging.getLogger("sjsgsr") |
| 18 | |
| 19 | # script starttime, used for timeboxing |
| 20 | starttime = datetime.datetime.now(datetime.timezone.utc) |
| 21 | startdate = datetime.datetime( |
| 22 | starttime.year, starttime.month, starttime.day, tzinfo=datetime.timezone.utc |
| 23 | ) |
| 24 | |
| 25 | |
| 26 | def 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 | |
| 64 | def 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 | |
| 156 | def clean_name(name): |
| 157 | """ |
| 158 | Clean up a name string. Currently only replaces spaces with underscores |
| 159 | """ |
| 160 | return name.replace(" ", "_") |
| 161 | |
| 162 | |
| 163 | def 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 | |
| 176 | def 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 |
| 191 | if __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 Williams | 0204788 | 2020-10-28 11:04:07 -0700 | [diff] [blame^] | 208 | # list of projects |
| 209 | projdirs = os.listdir(args.product_dir) |
Zack Williams | 712caf6 | 2020-04-28 13:37:41 -0700 | [diff] [blame] | 210 | |
Zack Williams | 0204788 | 2020-10-28 11:04:07 -0700 | [diff] [blame^] | 211 | for projdir in projdirs: |
| 212 | prodfiles = os.listdir("%s/%s" % (args.product_dir, projdir)) |
Zack Williams | 712caf6 | 2020-04-28 13:37:41 -0700 | [diff] [blame] | 213 | |
Zack Williams | 0204788 | 2020-10-28 11:04:07 -0700 | [diff] [blame^] | 214 | for prodfile in prodfiles: |
Zack Williams | 712caf6 | 2020-04-28 13:37:41 -0700 | [diff] [blame] | 215 | |
Zack Williams | 0204788 | 2020-10-28 11:04:07 -0700 | [diff] [blame^] | 216 | # 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 Williams | 712caf6 | 2020-04-28 13:37:41 -0700 | [diff] [blame] | 220 | |
Zack Williams | 0204788 | 2020-10-28 11:04:07 -0700 | [diff] [blame^] | 221 | projname = product["onf_project"] |
Zack Williams | 712caf6 | 2020-04-28 13:37:41 -0700 | [diff] [blame] | 222 | |
Zack Williams | 0204788 | 2020-10-28 11:04:07 -0700 | [diff] [blame^] | 223 | # 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 Williams | 712caf6 | 2020-04-28 13:37:41 -0700 | [diff] [blame] | 236 | |
| 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) |