blob: 1b88826ebf1b0a6e87ac3ae224880cec98d78452 [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
24import time
Matthew Hartfb6fd362020-03-04 21:03:59 +000025import yaml
Matthew Hart4a4f1202020-06-12 15:52:46 +010026import requests
27import shutil
Paul Sokolovskyb06bf6f2022-12-27 13:46:24 +030028import logging
29
30
31_log = logging.getLogger("lavaci")
32
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +010033
34class LAVA_RPC_connector(xmlrpc.client.ServerProxy, object):
35
36 def __init__(self,
37 username,
38 token,
39 hostname,
40 rest_prefix="RPC2",
41 https=False):
42
43 # If user provides hostname with http/s prefix
44 if "://" in hostname:
45 htp_pre, hostname = hostname.split("://")
46 server_addr = "%s://%s:%s@%s/%s" % (htp_pre,
47 username,
48 token,
49 hostname,
50 rest_prefix)
51 self.server_url = "%s://%s" % (htp_pre, hostname)
52 else:
53 server_addr = "%s://%s:%s@%s/%s" % ("https" if https else "http",
54 username,
55 token,
56 hostname,
57 rest_prefix)
58 self.server_url = "%s://%s" % ("https" if https else "http",
59 hostname)
60
61 self.server_job_prefix = "%s/scheduler/job/%%s" % self.server_url
Milosz Wasilewski4c4190d2020-12-15 12:56:22 +000062 self.server_api = "%s/api/v0.2/" % self.server_url
Matthew Hart4a4f1202020-06-12 15:52:46 +010063 self.server_results_prefix = "%s/results/%%s" % self.server_url
Matthew Hartc6bbbf92020-08-19 14:12:07 +010064 self.token = token
65 self.username = username
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +010066 super(LAVA_RPC_connector, self).__init__(server_addr)
67
68 def _rpc_cmd_raw(self, cmd, params=None):
69 """ Run a remote comand and return the result. There is no constrain
70 check on the syntax of the command. """
71
72 cmd = "self.%s(%s)" % (cmd, params if params else "")
73 return eval(cmd)
74
75 def ls_cmd(self):
76 """ Return a list of supported commands """
77
78 print("\n".join(self.system.listMethods()))
79
Matthew Hart4a4f1202020-06-12 15:52:46 +010080 def fetch_file(self, url, out_file):
Matthew Hartc6bbbf92020-08-19 14:12:07 +010081 auth_params = {
82 'user': self.username,
83 'token': self.token
84 }
Paul Sokolovsky903bc432022-12-29 17:15:04 +030085 with requests.get(url, stream=True, params=auth_params) as r:
86 r.raise_for_status()
87 with open(out_file, 'wb') as f:
88 shutil.copyfileobj(r.raw, f)
89 return(out_file)
Matthew Hart4a4f1202020-06-12 15:52:46 +010090
91 def get_job_results(self, job_id, yaml_out_file):
92 results_url = "{}/yaml".format(self.server_results_prefix % job_id)
93 return(self.fetch_file(results_url, yaml_out_file))
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +010094
Matthew Hartfb6fd362020-03-04 21:03:59 +000095 def get_job_definition(self, job_id, yaml_out_file=None):
96 job_def = self.scheduler.jobs.definition(job_id)
97 if yaml_out_file:
98 with open(yaml_out_file, "w") as F:
99 F.write(str(job_def))
Paul Sokolovskyf2f385d2022-01-11 00:36:31 +0300100 def_o = yaml.safe_load(job_def)
Xinyu Zhang82dab282022-10-09 16:33:19 +0800101 return def_o
Matthew Hartfb6fd362020-03-04 21:03:59 +0000102
Matthew Hart4a4f1202020-06-12 15:52:46 +0100103 def get_job_log(self, job_id, target_out_file):
Milosz Wasilewski4c4190d2020-12-15 12:56:22 +0000104 auth_headers = {"Authorization": "Token %s" % self.token}
105 log_url = "{server_url}/jobs/{job_id}/logs/".format(
106 server_url=self.server_api, job_id=job_id
107 )
Fathi Boudrac10378c2021-01-21 18:25:19 +0100108 with requests.get(log_url, stream=True, headers=auth_headers) as r:
Paul Sokolovsky903bc432022-12-29 17:15:04 +0300109 r.raise_for_status()
Fathi Boudrac10378c2021-01-21 18:25:19 +0100110 log_list = yaml.load(r.content, Loader=yaml.SafeLoader)
111 with open(target_out_file, "w") as target_out:
112 for line in log_list:
113 level = line["lvl"]
114 if (level == "target") or (level == "feedback"):
115 try:
116 target_out.write("{}\n".format(line["msg"]))
117 except UnicodeEncodeError:
118 msg = (
119 line["msg"]
120 .encode("ascii", errors="replace")
121 .decode("ascii")
122 )
123 target_out.write("{}\n".format(msg))
Matthew Hartfb6fd362020-03-04 21:03:59 +0000124
Matthew Hart4a4f1202020-06-12 15:52:46 +0100125 def get_job_config(self, job_id, config_out_file):
126 config_url = "{}/configuration".format(self.server_job_prefix % job_id)
127 self.fetch_file(config_url, config_out_file)
Matthew Hartfb6fd362020-03-04 21:03:59 +0000128
129 def get_job_info(self, job_id, yaml_out_file=None):
130 job_info = self.scheduler.jobs.show(job_id)
131 if yaml_out_file:
132 with open(yaml_out_file, "w") as F:
133 F.write(str(job_info))
134 return job_info
135
136 def get_error_reason(self, job_id):
Matthew Hart2c2688f2020-05-26 13:09:20 +0100137 try:
138 lava_res = self.results.get_testsuite_results_yaml(job_id, 'lava')
Paul Sokolovskyf2f385d2022-01-11 00:36:31 +0300139 results = yaml.safe_load(lava_res)
Matthew Hart2c2688f2020-05-26 13:09:20 +0100140 for test in results:
141 if test['name'] == 'job':
142 return(test.get('metadata', {}).get('error_type', ''))
143 except Exception:
144 return("Unknown")
Matthew Hartfb6fd362020-03-04 21:03:59 +0000145
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100146 def get_job_state(self, job_id):
147 return self.scheduler.job_state(job_id)["job_state"]
148
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100149 def cancel_job(self, job_id):
150 """ Cancell job with id=job_id. Returns True if successfull """
151
152 return self.scheduler.jobs.cancel(job_id)
153
154 def validate_job_yaml(self, job_definition, print_err=False):
155 """ Validate a job definition syntax. Returns true is server considers
156 the syntax valid """
157
158 try:
159 with open(job_definition) as F:
160 input_yaml = F.read()
161 self.scheduler.validate_yaml(input_yaml)
162 return True
163 except Exception as E:
164 if print_err:
165 print(E)
166 return False
167
Matthew Hart110e1dc2020-05-27 17:18:55 +0100168 def device_type_from_def(self, job_data):
Paul Sokolovskyf2f385d2022-01-11 00:36:31 +0300169 def_yaml = yaml.safe_load(job_data)
Matthew Hart110e1dc2020-05-27 17:18:55 +0100170 return(def_yaml['device_type'])
171
172 def has_device_type(self, job_data):
173 d_type = self.device_type_from_def(job_data)
174 all_d = self.scheduler.devices.list()
175 for device in all_d:
176 if device['type'] == d_type:
177 if device['health'] in ['Good', 'Unknown']:
178 return(True)
179 return(False)
180
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100181 def submit_job(self, job_definition):
182 """ Will submit a yaml definition pointed by job_definition after
183 validating it againist the remote backend. Returns resulting job id,
184 and server url for job"""
185
186 try:
187 if not self.validate_job_yaml(job_definition):
188 print("Served rejected job's syntax")
189 raise Exception("Invalid job")
190 with open(job_definition, "r") as F:
191 job_data = F.read()
192 except Exception as e:
193 print("Cannot submit invalid job. Check %s's content" %
194 job_definition)
195 print(e)
196 return None, None
Dean Bircha6ede7e2020-03-13 14:00:33 +0000197 try:
Dean Birch1d545c02020-05-29 14:09:21 +0100198 if self.has_device_type(job_data):
199 job_id = self.scheduler.submit_job(job_data)
200 job_url = self.server_job_prefix % job_id
201 return(job_id, job_url)
202 else:
203 raise Exception("No devices online with required device_type")
Dean Bircha6ede7e2020-03-13 14:00:33 +0000204 except Exception as e:
205 print(e)
206 return(None, None)
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100207
208 def resubmit_job(self, job_id):
209 """ Re-submit job with provided id. Returns resulting job id,
210 and server url for job"""
211
212 job_id = self.scheduler.resubmit_job(job_id)
213 job_url = self.server_job_prefix % job_id
214 return(job_id, job_url)
215
216 def block_wait_for_job(self, job_id, timeout, poll_freq=1):
217 """ Will block code execution and wait for the job to submit.
218 Returns job status on completion """
219
220 start_t = int(time.time())
221 while(True):
222 cur_t = int(time.time())
223 if cur_t - start_t >= timeout:
224 print("Breaking because of timeout")
225 break
226 # Check if the job is not running
Dean Arnoldf1169b92020-03-11 10:14:14 +0000227 cur_status = self.get_job_state(job_id)
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100228 # If in queue or running wait
Dean Arnoldc1d81b42020-03-11 15:56:36 +0000229 if cur_status not in ["Canceling","Finished"]:
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100230 time.sleep(poll_freq)
231 else:
232 break
Dean Arnoldc1d81b42020-03-11 15:56:36 +0000233 return self.scheduler.job_health(job_id)["job_health"]
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100234
Matthew Hartfb6fd362020-03-04 21:03:59 +0000235 def block_wait_for_jobs(self, job_ids, timeout, poll_freq=10):
236 """ Wait for multiple LAVA job ids to finish and return finished list """
237
238 start_t = int(time.time())
239 finished_jobs = {}
240 while(True):
241 cur_t = int(time.time())
242 if cur_t - start_t >= timeout:
243 print("Breaking because of timeout")
244 break
245 for job_id in job_ids:
Paul Sokolovskyfb298c62022-04-29 23:15:17 +0300246 if job_id in finished_jobs:
247 continue
Matthew Hartfb6fd362020-03-04 21:03:59 +0000248 # Check if the job is not running
Paul Sokolovsky81ff0ad2022-12-29 21:47:01 +0300249 try:
250 cur_status = self.get_job_info(job_id)
251 except xmlrpc.client.ProtocolError as e:
252 # There can be transient HTTP errors, e.g. "502 Proxy Error"
253 # Just continue with the next job, the faulted one will be
254 # re-checked on next iteration.
255 _log.warning("block_wait_for_jobs: xmlrpc.client.ProtocolError %s occurred, ignore and continue", e.errmsg)
256 time.sleep(2)
257 continue
Matthew Hartfb6fd362020-03-04 21:03:59 +0000258 # If in queue or running wait
259 if cur_status['state'] in ["Canceling","Finished"]:
260 cur_status['error_reason'] = self.get_error_reason(job_id)
261 finished_jobs[job_id] = cur_status
Paul Sokolovskyb06bf6f2022-12-27 13:46:24 +0300262 _log.info(
Paul Sokolovskyb7a41a92022-12-28 18:06:45 +0300263 "Job %d finished in %ds with state: %s, health: %s. Remaining: %d",
264 job_id, time.time() - start_t,
265 cur_status['state'],
266 cur_status['health'],
Paul Sokolovskyb06bf6f2022-12-27 13:46:24 +0300267 len(job_ids) - len(finished_jobs)
268 )
Matthew Hartfb6fd362020-03-04 21:03:59 +0000269 if len(job_ids) == len(finished_jobs):
270 break
271 else:
272 time.sleep(poll_freq)
273 if len(job_ids) == len(finished_jobs):
274 break
275 return finished_jobs
276
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +0100277 def test_credentials(self):
278 """ Attempt to querry the back-end and verify that the user provided
279 authentication is valid """
280
281 try:
282 self._rpc_cmd_raw("system.listMethods")
283 return True
284 except Exception as e:
285 print(e)
286 print("Credential validation failed")
287 return False
288
289
290if __name__ == "__main__":
291 pass