blob: e6c391eb6baf26869e4deebfda0f96fb03a80602 [file] [log] [blame]
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +01001#!/usr/bin/env python3
2
3""" lava_rpc_connector.py:
4
5 class that extends xmlrpc in order to add LAVA specific functionality.
6 Used in managing communication with the back-end. """
7
8from __future__ import print_function
9
10__copyright__ = """
11/*
Xinyu Zhang82dab282022-10-09 16:33:19 +080012 * Copyright (c) 2018-2022, Arm Limited. All rights reserved.
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +010013 *
14 * SPDX-License-Identifier: BSD-3-Clause
15 *
16 */
17 """
Karl Zhang08681e62020-10-30 13:56:03 +080018
19__author__ = "tf-m@lists.trustedfirmware.org"
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +010020__project__ = "Trusted Firmware-M Open CI"
Xinyu Zhang06286a92021-07-22 14:00:51 +080021__version__ = "1.4.0"
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +010022
23import xmlrpc.client
Paul Sokolovsky0c5e8da2024-03-06 12:18:02 +070024import os
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +010025import time
Paul Sokolovskyd042d9e2024-03-11 15:15:26 +070026import json
Matthew Hartfb6fd362020-03-04 21:03:59 +000027import yaml
Matthew Hart4a4f1202020-06-12 15:52:46 +010028import requests
29import shutil
Paul Sokolovsky0c5e8da2024-03-06 12:18:02 +070030import subprocess
Paul Sokolovskyb06bf6f2022-12-27 13:46:24 +030031import logging
32
33
34_log = logging.getLogger("lavaci")
35
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +010036
37class LAVA_RPC_connector(xmlrpc.client.ServerProxy, object):
38
39 def __init__(self,
40 username,
41 token,
42 hostname,
43 rest_prefix="RPC2",
44 https=False):
45
46 # If user provides hostname with http/s prefix
47 if "://" in hostname:
48 htp_pre, hostname = hostname.split("://")
49 server_addr = "%s://%s:%s@%s/%s" % (htp_pre,
50 username,
51 token,
52 hostname,
53 rest_prefix)
54 self.server_url = "%s://%s" % (htp_pre, hostname)
55 else:
56 server_addr = "%s://%s:%s@%s/%s" % ("https" if https else "http",
57 username,
58 token,
59 hostname,
60 rest_prefix)
61 self.server_url = "%s://%s" % ("https" if https else "http",
62 hostname)
63
64 self.server_job_prefix = "%s/scheduler/job/%%s" % self.server_url
Milosz Wasilewski4c4190d2020-12-15 12:56:22 +000065 self.server_api = "%s/api/v0.2/" % self.server_url
Matthew Hart4a4f1202020-06-12 15:52:46 +010066 self.server_results_prefix = "%s/results/%%s" % self.server_url
Matthew Hartc6bbbf92020-08-19 14:12:07 +010067 self.token = token
68 self.username = username
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +010069 super(LAVA_RPC_connector, self).__init__(server_addr)
70
71 def _rpc_cmd_raw(self, cmd, params=None):
72 """ Run a remote comand and return the result. There is no constrain
73 check on the syntax of the command. """
74
75 cmd = "self.%s(%s)" % (cmd, params if params else "")
76 return eval(cmd)
77
Paul Sokolovskyd042d9e2024-03-11 15:15:26 +070078 @staticmethod
79 def is_tux_id(job_id):
80 job_id = str(job_id)
81 if job_id.isdigit() and len(job_id) < 22:
82 return False
83 else:
84 return True
85
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +010086 def ls_cmd(self):
87 """ Return a list of supported commands """
88
89 print("\n".join(self.system.listMethods()))
90
Matthew Hart4a4f1202020-06-12 15:52:46 +010091 def fetch_file(self, url, out_file):
Matthew Hartc6bbbf92020-08-19 14:12:07 +010092 auth_params = {
93 'user': self.username,
94 'token': self.token
95 }
Paul Sokolovsky903bc432022-12-29 17:15:04 +030096 with requests.get(url, stream=True, params=auth_params) as r:
97 r.raise_for_status()
98 with open(out_file, 'wb') as f:
99 shutil.copyfileobj(r.raw, f)
100 return(out_file)
Matthew Hart4a4f1202020-06-12 15:52:46 +0100101
Paul Sokolovskyd042d9e2024-03-11 15:15:26 +0700102 def get_job_results(self, job_id, job_info, yaml_out_file):
103 if self.is_tux_id(job_id):
104 results_url = job_info["extra"]["download_url"] + "lava-results.yaml"
105 else:
106 results_url = "{}/yaml".format(self.server_results_prefix % job_id)
Matthew Hart4a4f1202020-06-12 15:52:46 +0100107 return(self.fetch_file(results_url, yaml_out_file))
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100108
Paul Sokolovskyd042d9e2024-03-11 15:15:26 +0700109 def get_job_definition(self, job_id, job_info, yaml_out_file=None):
110 if self.is_tux_id(job_id):
111 url = job_info["extra"]["download_url"] + job_info["extra"]["job_definition"]
112 with requests.get(url) as r:
113 r.raise_for_status()
114 job_def = r.text
115 else:
116 job_def = self.scheduler.jobs.definition(job_id)
117
Matthew Hartfb6fd362020-03-04 21:03:59 +0000118 if yaml_out_file:
119 with open(yaml_out_file, "w") as F:
120 F.write(str(job_def))
Paul Sokolovskyf2f385d2022-01-11 00:36:31 +0300121 def_o = yaml.safe_load(job_def)
Xinyu Zhang82dab282022-10-09 16:33:19 +0800122 return def_o
Matthew Hartfb6fd362020-03-04 21:03:59 +0000123
Matthew Hart4a4f1202020-06-12 15:52:46 +0100124 def get_job_log(self, job_id, target_out_file):
Paul Sokolovskyd042d9e2024-03-11 15:15:26 +0700125 if self.is_tux_id(job_id):
126 auth_headers = {}
Saheer Babu4ba7caf2025-02-10 12:03:12 +0000127 log_url = "https://storage.tuxsuite.com/public/{tuxsuite_group}/{tuxsuite_project}/tests/{job_id}/lava-logs.yaml".format(
128 tuxsuite_group = os.environ.get("TUXSUITE_GROUP"),
129 tuxsuite_project = os.environ.get("TUXSUITE_PROJECT"),
Paul Sokolovskyd042d9e2024-03-11 15:15:26 +0700130 job_id=job_id
131 )
132 else:
133 auth_headers = {"Authorization": "Token %s" % self.token}
134 log_url = "{server_url}/jobs/{job_id}/logs/".format(
135 server_url=self.server_api, job_id=job_id
136 )
Fathi Boudrac10378c2021-01-21 18:25:19 +0100137 with requests.get(log_url, stream=True, headers=auth_headers) as r:
Paul Sokolovsky903bc432022-12-29 17:15:04 +0300138 r.raise_for_status()
Fathi Boudrac10378c2021-01-21 18:25:19 +0100139 log_list = yaml.load(r.content, Loader=yaml.SafeLoader)
140 with open(target_out_file, "w") as target_out:
141 for line in log_list:
142 level = line["lvl"]
143 if (level == "target") or (level == "feedback"):
144 try:
145 target_out.write("{}\n".format(line["msg"]))
146 except UnicodeEncodeError:
147 msg = (
148 line["msg"]
149 .encode("ascii", errors="replace")
150 .decode("ascii")
151 )
152 target_out.write("{}\n".format(msg))
Matthew Hartfb6fd362020-03-04 21:03:59 +0000153
Matthew Hart4a4f1202020-06-12 15:52:46 +0100154 def get_job_config(self, job_id, config_out_file):
Paul Sokolovskyd042d9e2024-03-11 15:15:26 +0700155 if self.is_tux_id(job_id):
156 return
157
Matthew Hart4a4f1202020-06-12 15:52:46 +0100158 config_url = "{}/configuration".format(self.server_job_prefix % job_id)
159 self.fetch_file(config_url, config_out_file)
Matthew Hartfb6fd362020-03-04 21:03:59 +0000160
161 def get_job_info(self, job_id, yaml_out_file=None):
Paul Sokolovskyd042d9e2024-03-11 15:15:26 +0700162 if self.is_tux_id(job_id):
163 assert yaml_out_file is None
164 job_info = subprocess.check_output(
165 "python3 -u -m tuxsuite test get --json %s" % job_id,
166 shell=True,
167 )
168 job_info = json.loads(job_info.decode())
169 # Convert values to match LAVA output, as expected by
170 # the rest of code.
171 job_info["state"] = job_info["state"].capitalize()
172 job_info["health"] = {"pass": "Complete"}.get(job_info["result"], job_info["result"])
Paul Sokolovsky4ff31ab2024-03-21 13:36:31 +0700173 # There's no "job_name" aka "description" in Tux data, but we utilize
174 # the fact that it's included in the original name of the job definition
175 # file, that info included in the Tux data.
176 job_info["description"] = job_info["extra"]["job_definition"].split("/", 1)[1].split(".", 1)[0]
Paul Sokolovskyd042d9e2024-03-11 15:15:26 +0700177 return job_info
178
Matthew Hartfb6fd362020-03-04 21:03:59 +0000179 job_info = self.scheduler.jobs.show(job_id)
180 if yaml_out_file:
181 with open(yaml_out_file, "w") as F:
182 F.write(str(job_info))
183 return job_info
184
185 def get_error_reason(self, job_id):
Matthew Hart2c2688f2020-05-26 13:09:20 +0100186 try:
187 lava_res = self.results.get_testsuite_results_yaml(job_id, 'lava')
Paul Sokolovskyf2f385d2022-01-11 00:36:31 +0300188 results = yaml.safe_load(lava_res)
Matthew Hart2c2688f2020-05-26 13:09:20 +0100189 for test in results:
190 if test['name'] == 'job':
191 return(test.get('metadata', {}).get('error_type', ''))
192 except Exception:
193 return("Unknown")
Matthew Hartfb6fd362020-03-04 21:03:59 +0000194
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100195 def get_job_state(self, job_id):
196 return self.scheduler.job_state(job_id)["job_state"]
197
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100198 def cancel_job(self, job_id):
199 """ Cancell job with id=job_id. Returns True if successfull """
200
201 return self.scheduler.jobs.cancel(job_id)
202
203 def validate_job_yaml(self, job_definition, print_err=False):
204 """ Validate a job definition syntax. Returns true is server considers
205 the syntax valid """
206
207 try:
208 with open(job_definition) as F:
209 input_yaml = F.read()
210 self.scheduler.validate_yaml(input_yaml)
211 return True
212 except Exception as E:
213 if print_err:
214 print(E)
215 return False
216
Matthew Hart110e1dc2020-05-27 17:18:55 +0100217 def device_type_from_def(self, job_data):
Paul Sokolovskyf2f385d2022-01-11 00:36:31 +0300218 def_yaml = yaml.safe_load(job_data)
Matthew Hart110e1dc2020-05-27 17:18:55 +0100219 return(def_yaml['device_type'])
220
221 def has_device_type(self, job_data):
222 d_type = self.device_type_from_def(job_data)
223 all_d = self.scheduler.devices.list()
224 for device in all_d:
225 if device['type'] == d_type:
226 if device['health'] in ['Good', 'Unknown']:
227 return(True)
228 return(False)
229
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100230 def submit_job(self, job_definition):
231 """ Will submit a yaml definition pointed by job_definition after
232 validating it againist the remote backend. Returns resulting job id,
233 and server url for job"""
234
Saheer Babu4ba7caf2025-02-10 12:03:12 +0000235 tuxsuite_group = os.environ.get("TUXSUITE_GROUP")
236 tuxsuite_project = os.environ.get("TUXSUITE_PROJECT")
237
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100238 try:
239 if not self.validate_job_yaml(job_definition):
Paul Sokolovsky80b9b352024-03-05 16:38:41 +0700240 _log.error("Server rejected job's syntax")
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100241 raise Exception("Invalid job")
242 with open(job_definition, "r") as F:
243 job_data = F.read()
244 except Exception as e:
245 print("Cannot submit invalid job. Check %s's content" %
246 job_definition)
247 print(e)
248 return None, None
Paul Sokolovsky0c5e8da2024-03-06 12:18:02 +0700249
250 device_type = self.device_type_from_def(job_data)
251
Paul Sokolovskyd5870272024-04-01 12:14:17 +0700252 if device_type == "fvp" and os.environ.get("USE_TUXSUITE_FVP", "1") != "0":
Paul Sokolovsky0c5e8da2024-03-06 12:18:02 +0700253 output = subprocess.check_output(
254 "python3 -u -m tuxsuite test submit --no-wait --device fvp-lava --job-definition %s" % job_definition,
255 shell=True,
256 )
257
258 job_id = job_url = None
259 for l in output.decode().split("\n"):
260 _log.debug(l)
261 if l.startswith("uid:"):
262 job_id = l.split(None, 1)[1].strip()
Saheer Babu4ba7caf2025-02-10 12:03:12 +0000263 job_url = "https://tuxapi.tuxsuite.com/v1/groups/{tuxsuite_group}/projects/{tuxsuite_project}/tests/{job_id}".format(
264 tuxsuite_group = os.environ.get("TUXSUITE_GROUP"),
265 tuxsuite_project = os.environ.get("TUXSUITE_PROJECT"),
266 job_id=job_id
267 )
Paul Sokolovsky0c5e8da2024-03-06 12:18:02 +0700268 return (job_id, job_url)
269
Dean Bircha6ede7e2020-03-13 14:00:33 +0000270 try:
Dean Birch1d545c02020-05-29 14:09:21 +0100271 if self.has_device_type(job_data):
272 job_id = self.scheduler.submit_job(job_data)
273 job_url = self.server_job_prefix % job_id
274 return(job_id, job_url)
275 else:
276 raise Exception("No devices online with required device_type")
Dean Bircha6ede7e2020-03-13 14:00:33 +0000277 except Exception as e:
Paul Sokolovskyb2ca65b2024-03-11 15:07:34 +0700278 _log.exception("Exception submitting job to LAVA", e)
Dean Bircha6ede7e2020-03-13 14:00:33 +0000279 return(None, None)
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100280
281 def resubmit_job(self, job_id):
282 """ Re-submit job with provided id. Returns resulting job id,
283 and server url for job"""
284
285 job_id = self.scheduler.resubmit_job(job_id)
286 job_url = self.server_job_prefix % job_id
287 return(job_id, job_url)
288
289 def block_wait_for_job(self, job_id, timeout, poll_freq=1):
290 """ Will block code execution and wait for the job to submit.
291 Returns job status on completion """
292
293 start_t = int(time.time())
294 while(True):
295 cur_t = int(time.time())
296 if cur_t - start_t >= timeout:
297 print("Breaking because of timeout")
298 break
299 # Check if the job is not running
Dean Arnoldf1169b92020-03-11 10:14:14 +0000300 cur_status = self.get_job_state(job_id)
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100301 # If in queue or running wait
Dean Arnoldc1d81b42020-03-11 15:56:36 +0000302 if cur_status not in ["Canceling","Finished"]:
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100303 time.sleep(poll_freq)
304 else:
305 break
Dean Arnoldc1d81b42020-03-11 15:56:36 +0000306 return self.scheduler.job_health(job_id)["job_health"]
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100307
Paul Sokolovskyf9bad0d2024-03-25 15:17:38 +0700308 def block_wait_for_jobs(self, job_ids, timeout, poll_freq=10, callback=None):
Matthew Hartfb6fd362020-03-04 21:03:59 +0000309 """ Wait for multiple LAVA job ids to finish and return finished list """
310
311 start_t = int(time.time())
312 finished_jobs = {}
313 while(True):
314 cur_t = int(time.time())
315 if cur_t - start_t >= timeout:
316 print("Breaking because of timeout")
317 break
318 for job_id in job_ids:
Paul Sokolovskyfb298c62022-04-29 23:15:17 +0300319 if job_id in finished_jobs:
320 continue
Matthew Hartfb6fd362020-03-04 21:03:59 +0000321 # Check if the job is not running
Paul Sokolovsky81ff0ad2022-12-29 21:47:01 +0300322 try:
323 cur_status = self.get_job_info(job_id)
Paul Sokolovskyc82f9332023-01-10 23:50:25 +0300324 except (xmlrpc.client.ProtocolError, OSError) as e:
Paul Sokolovsky81ff0ad2022-12-29 21:47:01 +0300325 # There can be transient HTTP errors, e.g. "502 Proxy Error"
Paul Sokolovskyc82f9332023-01-10 23:50:25 +0300326 # or socket timeout.
Paul Sokolovsky81ff0ad2022-12-29 21:47:01 +0300327 # Just continue with the next job, the faulted one will be
328 # re-checked on next iteration.
Paul Sokolovskyc82f9332023-01-10 23:50:25 +0300329 _log.warning("block_wait_for_jobs: %r occurred, ignore and continue", e)
Paul Sokolovsky81ff0ad2022-12-29 21:47:01 +0300330 time.sleep(2)
331 continue
Matthew Hartfb6fd362020-03-04 21:03:59 +0000332 # If in queue or running wait
333 if cur_status['state'] in ["Canceling","Finished"]:
334 cur_status['error_reason'] = self.get_error_reason(job_id)
335 finished_jobs[job_id] = cur_status
Paul Sokolovskyb06bf6f2022-12-27 13:46:24 +0300336 _log.info(
Paul Sokolovsky6e83a232024-03-11 15:30:04 +0700337 "Job %s finished in %ds with state: %s, health: %s. Remaining: %d",
Paul Sokolovskyb7a41a92022-12-28 18:06:45 +0300338 job_id, time.time() - start_t,
339 cur_status['state'],
340 cur_status['health'],
Paul Sokolovskyb06bf6f2022-12-27 13:46:24 +0300341 len(job_ids) - len(finished_jobs)
342 )
Paul Sokolovskyf9bad0d2024-03-25 15:17:38 +0700343 if callback:
344 callback(job_id, cur_status)
Matthew Hartfb6fd362020-03-04 21:03:59 +0000345 if len(job_ids) == len(finished_jobs):
346 break
347 else:
348 time.sleep(poll_freq)
349 if len(job_ids) == len(finished_jobs):
350 break
351 return finished_jobs
352
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100353 def test_credentials(self):
354 """ Attempt to querry the back-end and verify that the user provided
355 authentication is valid """
356
357 try:
358 self._rpc_cmd_raw("system.listMethods")
359 return True
360 except Exception as e:
361 print(e)
362 print("Credential validation failed")
363 return False
364
365
366if __name__ == "__main__":
367 pass