blob: 737626d5e535d8115bd4c4a98ed89d8240de4bdc [file] [log] [blame]
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +01001#!/usr/bin/env python3 -u
2
3""" lava_helper.py:
4
5 Generate custom defined LAVA definitions redered from Jinja2 templates.
6 It can also parse the yaml output of LAVA and verify the test outcome """
7
8from __future__ import print_function
9
10__copyright__ = """
11/*
12 * Copyright (c) 2018-2019, Arm Limited. All rights reserved.
13 *
14 * SPDX-License-Identifier: BSD-3-Clause
15 *
16 */
17 """
18__author__ = "Minos Galanakis"
19__email__ = "minos.galanakis@linaro.org"
20__project__ = "Trusted Firmware-M Open CI"
21__status__ = "stable"
Minos Galanakisea421232019-06-20 17:11:28 +010022__version__ = "1.1"
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +010023
24import os
25import sys
26import argparse
27from copy import deepcopy
28from collections import OrderedDict
29from jinja2 import Environment, FileSystemLoader
30from lava_helper_configs import *
31
32try:
33 from tfm_ci_pylib.utils import save_json, load_json, sort_dict,\
Minos Galanakisea421232019-06-20 17:11:28 +010034 load_yaml, test, print_test
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +010035 from tfm_ci_pylib.lava_rpc_connector import LAVA_RPC_connector
36except ImportError:
37 dir_path = os.path.dirname(os.path.realpath(__file__))
38 sys.path.append(os.path.join(dir_path, "../"))
39 from tfm_ci_pylib.utils import save_json, load_json, sort_dict,\
Minos Galanakisea421232019-06-20 17:11:28 +010040 load_yaml, test, print_test
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +010041 from tfm_ci_pylib.lava_rpc_connector import LAVA_RPC_connector
42
43
44def sort_lavagen_config(cfg):
45 """ Create a constact dictionary object. This method is tailored for
46 the complicated configuration structure of this module """
47
48 res = OrderedDict()
49 if sorted(lavagen_config_sort_order) == sorted(cfg.keys()):
50 item_list = sorted(cfg.keys(),
51 key=lambda x: lavagen_config_sort_order.index(x))
52 else:
53 item_list = sorted(cfg.keys(), key=len)
54 for k in item_list:
55 v = cfg[k]
56 if isinstance(v, dict):
57 res[k] = sort_lavagen_config(v)
58 elif isinstance(v, list) and isinstance(v[0], dict):
59 res[k] = [sort_dict(e, lava_gen_monitor_sort_order) for e in v]
60 else:
61 res[k] = v
62 return res
63
64
65def save_config(cfg_f, cfg_obj):
66 """ Export configuration to json file """
67 save_json(cfg_f, sort_lavagen_config(cfg_obj))
68
69
70def print_configs():
71 """ Print supported configurations """
72
73 print("%(pad)s Built-in configurations: %(pad)s" % {"pad": "*" * 10})
74 for k in lava_gen_config_map.keys():
75 print("\t * %s" % k)
76
77
78def generate_test_definitions(config, work_dir):
79 """ Get a dictionary configuration, and an existing jinja2 template
80 and generate a LAVA compatbile yaml definition """
81
82 template_loader = FileSystemLoader(searchpath=work_dir)
83 template_env = Environment(loader=template_loader)
84
85 # Ensure that the jinja2 template is always rendered the same way
86 config = sort_lavagen_config(config)
87
88 template_file = config.pop("templ")
89
90 definition = template_env.get_template(template_file).render(**config)
91 return definition
92
93
94def generate_lava_job_defs(user_args, config):
95 """ Create a LAVA test job definition file """
96
97 # Evaluate current directory
98 if user_args.work_dir:
99 work_dir = os.path.abspath(user_args.work_dir)
100 else:
101 work_dir = os.path.abspath(os.path.dirname(__file__))
102
103 # If a single platform is requested and it exists in the platform
104 if user_args.platform and user_args.platform in config["platforms"]:
105 # Only test this platform
106 platform = user_args.platform
107 config["platforms"] = {platform: config["platforms"][platform]}
108
109 # Generate the ouptut definition
110 definition = generate_test_definitions(config, work_dir)
111
112 # Write it into a file
113 out_file = os.path.abspath(user_args.lava_def_output)
114 with open(out_file, "w") as F:
115 F.write(definition)
116
117 print("Definition created at %s" % out_file)
118
119
120def test_map_from_config(lvg_cfg=tfm_mps2_sse_200):
121 """ Extract all required information from a lavagen config map
122 and generate a map of required tests, indexed by test name """
123
124 test_map = {}
125 suffix_l = []
126 for p in lvg_cfg["platforms"]:
127 for c in lvg_cfg["compilers"]:
128 for bd in lvg_cfg["build_types"]:
129 for bt in lvg_cfg["boot_types"]:
130 suffix_l.append("%s_%s_%s_%s_%s" % (p, c, "%s", bd, bt))
131
132 for test_cfg_name, tst in lvg_cfg["tests"].items():
133 for monitor in tst["monitors"]:
134 for suffix in suffix_l:
135 key = (monitor["name"] + "_" + suffix % test_cfg_name).lower()
136 # print (monitor['required'])
137 test_map[key] = monitor['required']
138
139 return deepcopy(test_map)
140
141
142def test_lava_results(user_args, config):
143 """ Uses input of a test config dictionary and a LAVA summary Files
144 and determines if the test is a successful or not """
145
146 # Parse command line arguments to override config
147 result_raw = load_yaml(user_args.lava_results)
148
149 test_map = test_map_from_config(config)
150 t_dict = {k: {} for k in test_map}
151
152 # Return true if test is contained in test_groups
153 def test_filter(x):
154 return x["metadata"]['definition'] in test_map
155
156 # Create a dictionary with common keys as the test map and test results
157 # {test_suite: {test_name: pass/fail}}
158 def format_results(x):
159 t_dict[x["metadata"]["definition"]].update({x["metadata"]["case"]:
160 x["metadata"]["result"]})
161
162 # Remove all irelevant entries from data
163 test_results = list(filter(test_filter, result_raw))
164
165 # Call the formatter
166 list(map(format_results, test_results))
167
Minos Galanakisea421232019-06-20 17:11:28 +0100168 # Remove the ignored commits if requested
169 if user_args.ignore_configs:
170 print(user_args.ignore_configs)
171 for cfg in user_args.ignore_configs:
172 try:
173 print("Rejecting config: ", cfg)
174 t_dict.pop(cfg)
175 except KeyError as e:
176 print("Warning! Rejected config %s not found"
177 " in LAVA results" % cfg)
178
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100179 # We need to check that each of the tests contained in the test_map exist
180 # AND that they have a passed status
181 t_sum = 0
Minos Galanakisea421232019-06-20 17:11:28 +0100182
183 with open("lava_job.url", "r") as F:
184 job_url = F.read().strip()
185
186 out_rep = {"report": {},
187 "_metadata_": {"job_url": job_url}}
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100188 for k, v in t_dict.items():
189 try:
Minos Galanakisea421232019-06-20 17:11:28 +0100190 out_rep["report"][k] = test(test_map[k],
191 v,
192 pass_text=["pass"],
193 error_on_failed=False,
194 test_name=k,
195 summary=user_args.lava_summary)
196 t_sum += int(out_rep["report"][k]["success"])
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100197 # Status can be None if a test did't fully run/complete
198 except TypeError as E:
199 t_sum = 1
Minos Galanakisea421232019-06-20 17:11:28 +0100200 print("\n")
201 sl = [x["name"] for x in out_rep["report"].values()
202 if x["success"] is True]
203 fl = [x["name"] for x in out_rep["report"].values()
204 if x["success"] is False]
205
206 if sl:
207 print_test(t_list=sl, status="passed", tname="Tests")
208 if fl:
209 print_test(t_list=fl, status="failed", tname="Tests")
210
211 # Generate the output report is requested
212 if user_args.output_report:
213 save_json(user_args.output_report, out_rep)
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100214
215 # Every single of the tests need to have passed for group to succeed
216 if t_sum != len(t_dict):
217 print("Group Testing FAILED!")
Minos Galanakisea421232019-06-20 17:11:28 +0100218 if user_args.eif:
219 sys.exit(1)
220 else:
221 print("Group Testing PASS!")
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100222
223
224def test_lava_dispatch_credentials(user_args):
225 """ Will validate if provided token/credentials are valid. It will return
226 a valid connection or exit program if not"""
227
228 # Collect the authentication tokens
229 try:
230 if user_args.token_from_env:
231 usr = os.environ['LAVA_USER']
232 secret = os.environ['LAVA_TOKEN']
233 elif user_args.token_usr and user_args.token_secret:
234 usr = user_args.token_usr
235 secret = user_args.token_secret
236
237 # Do not submit job without complete credentials
238 if not len(usr) or not len(secret):
239 raise Exception("Credentials not set")
240
241 lava = LAVA_RPC_connector(usr,
242 secret,
243 user_args.lava_url,
244 user_args.lava_rpc)
245
246 # Test the credentials againist the backend
247 if not lava.test_credentials():
248 raise Exception("Server rejected user authentication")
249 except Exception as e:
250 print("Credential validation failed with : %s" % e)
251 print("Did you set set --lava_token_usr, --lava_token_secret?")
252 sys.exit(1)
253 return lava
254
255
256def lava_dispatch(user_args):
257 """ Submit a job to LAVA backend, block untill it is completed, and
258 fetch the results files if successful. If not, calls sys exit with 1
259 return code """
260
261 lava = test_lava_dispatch_credentials(user_args)
262 job_id, job_url = lava.submit_job(user_args.dispatch)
Minos Galanakisea421232019-06-20 17:11:28 +0100263
264 # The reason of failure will be reported to user by LAVA_RPC_connector
265 if job_id is None and job_url is None:
266 sys.exit(1)
267 else:
268 print("Job submitted at: " + job_url)
269
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100270 with open("lava_job.id", "w") as F:
271 F.write(str(job_id))
272 print("Job id %s stored at lava_job.id file." % job_id)
Minos Galanakisea421232019-06-20 17:11:28 +0100273 with open("lava_job.url", "w") as F:
274 F.write(str(job_url))
275 print("Job url %s stored at lava_job.url file." % job_id)
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100276
277 # Wait for the job to complete
278 status = lava.block_wait_for_job(job_id, int(user_args.dispatch_timeout))
279 print("Job %s returned with status: %s" % (job_id, status))
280 if status == "Complete":
281 lava.get_job_results(job_id, user_args.lava_job_results)
282 print("Job results exported at: %s" % user_args.lava_job_results)
283 sys.exit(0)
284 sys.exit(1)
285
286
287def dispatch_cancel(user_args):
288 """ Sends a cancell request for user provided job id (dispatch_cancel)"""
289 lava = test_lava_dispatch_credentials(user_args)
290 id = user_args.dispatch_cancel
291 result = lava.cancel_job(id)
292 print("Request to cancell job: %s returned with status %s" % (id, result))
293
294
295def load_config_overrides(user_args):
296 """ Load a configuration from multiple locations and override it with
297 user provided arguemnts """
298
299 if user_args.config_file:
300 print("Loading config from file %s" % user_args.config_file)
301 try:
302 config = load_json(user_args.config_file)
303 except Exception:
304 print("Failed to load config from: %s ." % user_args.config_file)
305 sys.exit(1)
306 else:
307 print("Using built-in config: %s" % user_args.config_key)
308 try:
309 config = lava_gen_config_map[user_args.config_key]
310 except KeyError:
311 print("No template found for config: %s" % user_args.config_key)
312 sys.exit(1)
313
314 config["build_no"] = user_args.build_no
315
Minos Galanakisea421232019-06-20 17:11:28 +0100316 # Override with command line provided URL/Job Name
317 if user_args.jenkins_url:
318 _over_d = {"jenkins_url": user_args.jenkins_url,
319 "jenkins_job": "%(jenkins_job)s"}
320 config["recovery_store_url"] = config["recovery_store_url"] % _over_d
321 config["artifact_store_url"] = config["artifact_store_url"] % _over_d
322
323 if user_args.jenkins_job:
324 _over_d = {"jenkins_job": user_args.jenkins_job}
325 config["recovery_store_url"] = config["recovery_store_url"] % _over_d
326 config["artifact_store_url"] = config["artifact_store_url"] % _over_d
327
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100328 # Add the template folder
329 config["templ"] = os.path.join(user_args.template_dir, config["templ"])
330 return config
331
332
333def main(user_args):
334 """ Main logic, forked according to task arguments """
335
336 # If a configuration listing is requested
337 if user_args.ls_config:
338 print_configs()
339 return
340 elif user_args.cconfig:
341 config_key = user_args.cconfig
342 if config_key in lava_gen_config_map.keys():
343 config_file = "lava_job_gen_cfg_%s.json" % config_key
344 save_config(config_file, lava_gen_config_map[config_key])
345 print("Configuration exported at %s" % config_file)
346 return
Minos Galanakisea421232019-06-20 17:11:28 +0100347 if user_args.dispatch is not None or user_args.dispatch_cancel is not None:
348 pass
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100349 else:
350 config = load_config_overrides(user_args)
351
352 # Configuration is assumed fixed at this point
353 if user_args.lava_results:
354 print("Evaluating File", user_args.lava_results)
355 test_lava_results(user_args, config)
356 elif user_args.dispatch:
357 lava_dispatch(user_args)
358 elif user_args.dispatch_cancel:
359 dispatch_cancel(user_args)
360 elif user_args.create_definition:
361 print("Generating Lava")
362 generate_lava_job_defs(user_args, config)
363 else:
364 print("Nothing to do, please select a task")
365
366
367def get_cmd_args():
368 """ Parse command line arguments """
369
370 # Parse command line arguments to override config
371 parser = argparse.ArgumentParser(description="Lava Helper")
372
373 def_g = parser.add_argument_group('Create LAVA Definition')
374 disp_g = parser.add_argument_group('Dispatch LAVA job')
375 parse_g = parser.add_argument_group('Parse LAVA results')
376 config_g = parser.add_argument_group('Configuration handling')
377 over_g = parser.add_argument_group('Overrides')
378
379 # Configuration control
380 config_g.add_argument("-cn", "--config-name",
381 dest="config_key",
382 action="store",
383 default="tfm_mps2_sse_200",
384 help="Select built-in configuration by name")
385 config_g.add_argument("-cf", "--config-file",
386 dest="config_file",
387 action="store",
388 help="Load config from external file in JSON format")
389 config_g.add_argument("-te", "--task-config-export",
390 dest="cconfig",
391 action="store",
392 help="Export a json file with the current config "
393 "parameters")
394 config_g.add_argument("-tl", "--task-config-list",
395 dest="ls_config",
396 action="store_true",
397 default=False,
398 help="List built-in configurations")
399
400 def_g.add_argument("-tc", "--task-create-definition",
401 dest="create_definition",
402 action="store_true",
403 default=False,
404 help="Used in conjunction with --config parameters. "
405 "A LAVA compatible job definition will be created")
406 def_g.add_argument("-cb", "--create-definition-build-no",
407 dest="build_no",
408 action="store",
409 default="lastSuccessfulBuild",
410 help="JENKINGS Build number selector. "
411 "Default: lastSuccessfulBuild")
412 def_g.add_argument("-co", "--create-definition-output-file",
413 dest="lava_def_output",
414 action="store",
415 default="job_results.yaml",
416 help="Set LAVA compatible .yaml output file")
417
418 # Parameter override commands
419 over_g.add_argument("-ow", "--override-work-path",
420 dest="work_dir",
421 action="store",
422 help="Working Directory (absolute path)")
423 over_g.add_argument("-ot", "--override-template-dir",
424 dest="template_dir",
425 action="store",
426 default="jinja2_templates",
427 help="Set directory where Jinja2 templates are stored")
428 over_g.add_argument("-op", "--override-platform",
429 dest="platform",
430 action="store",
431 help="Override platform.Only the provided one "
Minos Galanakisea421232019-06-20 17:11:28 +0100432 "will be tested")
433 over_g.add_argument("-ou", "--override-jenkins-url",
434 dest="jenkins_url",
435 action="store",
Minos Galanakis44074242019-10-14 09:59:11 +0100436 help="Override %%(jenkins_url)s params in config if "
Minos Galanakisea421232019-06-20 17:11:28 +0100437 "present. Sets the jenkings address including "
438 "port")
439 over_g.add_argument("-oj", "--override-jenkins-job",
440 dest="jenkins_job",
441 action="store",
Minos Galanakis44074242019-10-14 09:59:11 +0100442 help="Override %%(jenkins_job)s params in config if "
Minos Galanakisea421232019-06-20 17:11:28 +0100443 "present. Sets the jenkings job name")
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100444 parse_g.add_argument("-tp", "--task-lava-parse",
445 dest="lava_results",
446 action="store",
447 help="Parse provided yaml file, using a configuration"
448 " as reference to determine the outpcome"
449 " of testing")
450 parse_g.add_argument("-ls", "--lava-parse-summary",
451 dest="lava_summary",
Minos Galanakisea421232019-06-20 17:11:28 +0100452 default=False,
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100453 action="store_true",
454 help="Print full test summary")
Minos Galanakisea421232019-06-20 17:11:28 +0100455 parse_g.add_argument("-or", "--output-report",
456 dest="output_report",
457 action="store",
458 help="Print full test summary")
459 parser.add_argument("-ef", "--error-if-failed",
460 dest="eif",
461 action="store_true",
462 help="If set will change the script exit code if one "
463 "or more tests fail")
464 parser.add_argument('-ic', '--ignore-configs',
465 dest="ignore_configs",
466 nargs='+',
467 help="Pass a space separated list of build"
468 "configurations which will get ignored when"
469 "evaluation LAVA results")
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100470
471 # Lava job control commands
472 disp_g.add_argument("-td", "--task-dispatch",
473 dest="dispatch",
474 action="store",
475 help="Submit yaml file defined job to backend, and "
476 "wait for it to complete. \nRequires:"
477 " --lava_url --lava_token_usr/pass/--"
478 "lava_token_from_environ arguments, with optional"
479 "\n--lava_rpc_prefix\n--lava-job-results\n"
480 "parameters. \nIf not set they get RPC2 and "
481 "lava_job_results.yaml default values.\n"
482 "The number job id will be stored at lava_job.id")
483 disp_g.add_argument("-dc", "--dispatch-cancel",
484 dest="dispatch_cancel",
485 action="store",
486 help="Send a cancell request for job with provided id")
487 disp_g.add_argument("-dt", "--dispatch-timeout",
488 dest="dispatch_timeout",
489 default="3600",
490 action="store",
491 help="Maximum Time to block for job"
492 " submission to complete")
493 disp_g.add_argument("-dl", "--dispatch-lava-url",
494 dest="lava_url",
495 action="store",
496 help="Sets the lava hostname during job dispatch")
497 disp_g.add_argument("-dr", "--dispatch-lava-rpc-prefix",
498 dest="lava_rpc",
499 action="store",
500 default="RPC2",
501 help="Application prefix on Backend"
502 "(i.e www.domain.com/APP)\n"
503 "By default set to RPC2")
504 disp_g.add_argument("-du", "--dispatch-lava_token_usr",
505 dest="token_usr",
506 action="store",
507 help="Lava user submitting the job")
508 disp_g.add_argument("-ds", "--dispatch-lava_token_secret",
509 dest="token_secret",
510 action="store",
511 help="Hash token used to authenticate"
512 "user during job submission")
513 disp_g.add_argument("-de", "--dispatch-lava_token_from_environ",
514 dest="token_from_env",
515 action="store_true",
516 help="If set dispatcher will use the enviroment"
517 "stored $LAVA_USER, $LAVA_TOKEN for credentials")
518 disp_g.add_argument("-df", "--dispatch-lava-job-results-file",
519 dest="lava_job_results",
520 action="store",
521 default="lava_job_results.yaml",
522 help="Name of the job results file after job is "
523 "complete. Default: lava_job_results.yaml")
524 return parser.parse_args()
525
526
527if __name__ == "__main__":
528 main(get_cmd_args())