blob: 508704f276a83da5fe249160e6f8b45784024af6 [file] [log] [blame]
Dennis Dmitriev61115112018-05-31 06:48:43 +03001from __future__ import print_function
Dmitry Tyzhnenkob39de052019-03-21 17:05:07 +02002import datetime
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +03003import time
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +02004
5import jenkins
Dennis Dmitriev27a96792018-07-30 07:52:03 +03006import json
Hanna Arhipova874c68f2021-03-29 15:57:19 +03007import yaml
Dennis Dmitriev61115112018-05-31 06:48:43 +03008import requests
Anna Arhipova839a55f2023-06-19 10:48:00 +02009import re
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020010
11from devops.helpers import helpers
12
Dmitry Tyzhnenko80ce0202019-02-07 13:27:19 +020013from requests.exceptions import ConnectionError
14
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020015
Hanna Arhipovab2522692020-09-23 15:25:11 +030016def retry(max_count=6,
17 sleep_before_retry=10):
18 def _retry(func):
19 def __retry(*args, **kwargs):
20 """
21 Waits some time and retries the requests if it fails with
22 with any error
23 Raises Exceptions after all unsuccessful tries
24
25 :param func: callable
26 :param args, kwargs: parameters of decorated functions
27 :param max_count: times of retries
28 :param sleep_before_retry: how many seconds needs to wait before
29 the next retry
30 :return: response
31 :raise ConnectionError after several unsuccessful connections
32 """
Hanna Arhipova6d788142021-03-12 11:46:47 +020033 err_msg = None
Hanna Arhipovab2522692020-09-23 15:25:11 +030034 for count in range(max_count):
35 try:
36 return func(*args, **kwargs)
37 except Exception as err:
Hanna Arhipova6d788142021-03-12 11:46:47 +020038 err_msg = err
39 print("Try {count}/{max_count} caught "
40 "Exception in {fn}: {err}."
41 "\n... repeat after {secs} secs".
Hanna Arhipovab2522692020-09-23 15:25:11 +030042 format(err=err,
43 count=count+1,
44 max_count=max_count,
Hanna Arhipova6d788142021-03-12 11:46:47 +020045 secs=sleep_before_retry,
46 fn=func.__name__))
Hanna Arhipovab2522692020-09-23 15:25:11 +030047 time.sleep(sleep_before_retry)
48 print("Function failed in {total_time} seconds".format(
49 total_time=max_count*sleep_before_retry)
50 )
Hanna Arhipova6d788142021-03-12 11:46:47 +020051 raise err_msg
Hanna Arhipovab2522692020-09-23 15:25:11 +030052 return __retry
53 return _retry
54
55
56@retry(max_count=5, sleep_before_retry=2)
Hanna Arhipovae1f0b472020-01-03 17:38:11 +020057def send_request(action, endpoint):
58 """
Hanna Arhipovab2522692020-09-23 15:25:11 +030059 Makes request with described operation to endpoint
60 :param action: string, type of operation GET, POST, DELETE and so on
61 :param endpoint: string, url to send request
62 :return: response
Hanna Arhipovae1f0b472020-01-03 17:38:11 +020063 """
Hanna Arhipovab2522692020-09-23 15:25:11 +030064 response = requests.Request(action, endpoint)
65 return response
Hanna Arhipovae1f0b472020-01-03 17:38:11 +020066
67
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020068class JenkinsClient(object):
69
Dennis Dmitriev27a96792018-07-30 07:52:03 +030070 def __init__(self, host=None, username='admin', password='r00tme'):
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020071 host = host or 'http://172.16.44.33:8081'
obutenkoca858402019-07-04 18:31:39 +030072 self.__client = jenkins.Jenkins(
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020073 host,
74 username=username,
75 password=password)
obutenkoca858402019-07-04 18:31:39 +030076 self.__client._session.verify = False
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020077
Hanna Arhipovab2522692020-09-23 15:25:11 +030078 @retry()
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020079 def jobs(self):
80 return self.__client.get_jobs()
81
82 def find_jobs(self, name):
83 return filter(lambda x: name in x['fullname'], self.jobs())
84
Hanna Arhipovab2522692020-09-23 15:25:11 +030085 @retry()
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020086 def job_info(self, name):
Hanna Arhipovab2522692020-09-23 15:25:11 +030087 return self.__client.get_job_info(name)
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020088
89 def list_builds(self, name):
90 return self.job_info(name).get('builds')
91
Hanna Arhipovab2522692020-09-23 15:25:11 +030092 @retry()
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020093 def build_info(self, name, build_id):
Hanna Arhipovab2522692020-09-23 15:25:11 +030094 return self.__client.get_build_info(name, build_id)
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020095
96 def job_params(self, name):
97 job = self.job_info(name)
98 job_params = next(
99 p for p in job['property'] if
100 'hudson.model.ParametersDefinitionProperty' == p['_class'])
101 job_params = job_params['parameterDefinitions']
102 return job_params
103
Hanna Arhipova874c68f2021-03-29 15:57:19 +0300104 def make_defaults_params(self, name):
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200105 job_params = self.job_params(name)
106 def_params = dict(
107 [(j['name'], j['defaultParameterValue']['value'])
108 for j in job_params])
109 return def_params
110
Hanna Arhipova874c68f2021-03-29 15:57:19 +0300111 def _correct_yaml_params(self, job_name, params):
112 """
113 Params can be defined as a nested dict.
114 In that case 2nd-layer dict will be translated to YAML text and
115 added to default parameter value
116
117 :param job_name: Job name
118 :param params: dict of JenkinsJobs parameters
119 :return: nothing
120 """
121 for param_name, param_value in params.items():
122 if not isinstance(param_value, dict):
123 continue
124 default_param = self.make_defaults_params(job_name).get(param_name)
125 if default_param is None:
126 print("{param} param of {job} job doesn't exist. "
127 "Ignoring enriching it with {value}".format(
128 param=param_name,
129 job=job_name,
130 value=param_value
131 ))
132 continue
133 yaml_param = yaml.load(default_param)
134 yaml_param.update(param_value)
135 params[param_name] = yaml.dump(yaml_param,
136 default_flow_style=False)
137 return params
138
Hanna Arhipovab2522692020-09-23 15:25:11 +0300139 @retry()
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300140 def run_build(self, name, params=None, timeout=600, verbose=False):
Hanna Arhipova874c68f2021-03-29 15:57:19 +0300141 params = params or self.make_defaults_params(name)
142 params = self._correct_yaml_params(job_name=name,
143 params=params)
144
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300145 num = self.__client.build_job(name, params)
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300146 time.sleep(2) # wait while job is started
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300147
Dmitry Tyzhnenkob39de052019-03-21 17:05:07 +0200148 def is_build_queued():
149 try:
150 item = self.__client.get_queue_item(num)
151 ts = item['inQueueSince'] / 1000
152 since_time = datetime.datetime.fromtimestamp(ts)
153 print("Build in the queue since {}".format(since_time))
154 return True
155 except jenkins.JenkinsException:
156 if verbose:
157 print("Build have not been queued {} yet".format(num))
158
159 helpers.wait(
160 is_build_queued,
161 timeout=timeout,
162 interval=30,
163 timeout_msg='Timeout waiting to queue the build '
164 'for {} job'.format(name))
165
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300166 def is_blocked():
167 queued = self.__client.get_queue_item(num)
168 status = not queued['blocked']
169 if not status and verbose:
170 print("pending the job [{}] : {}".format(name, queued['why']))
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300171 return (status and
172 'executable' in (queued or {}) and
173 'number' in (queued['executable'] or {}))
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300174
175 helpers.wait(
176 is_blocked,
177 timeout=timeout,
178 interval=30,
179 timeout_msg='Timeout waiting to run the job [{}]'.format(name))
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300180 build_id = self.__client.get_queue_item(num)['executable']['number']
Dmitry Tyzhnenkob39de052019-03-21 17:05:07 +0200181
182 def is_build_started():
183 try:
184 build = self.__client.get_build_info(name, build_id)
185 ts = float(build['timestamp']) / 1000
186 start_time = datetime.datetime.fromtimestamp(ts)
187 print("the build {} in {} have started at {} UTC".format(
188 build_id, name, start_time))
189 return True
190 except jenkins.JenkinsException:
191 if verbose:
192 print("the build {} in {} have not strated yet".format(
193 build_id, name))
194 helpers.wait(
195 is_build_started,
196 timeout=timeout,
197 interval=30,
198 timeout_msg='Timeout waiting to run build of '
199 'the job [{}]'.format(name))
200
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200201 return name, build_id
202
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300203 def wait_end_of_build(self, name, build_id, timeout=600, interval=5,
204 verbose=False, job_output_prefix=''):
205 '''Wait until the specified build is finished
206
207 :param name: ``str``, job name
208 :param build_id: ``int``, build id
209 :param timeout: ``int``, timeout waiting the job, sec
210 :param interval: ``int``, interval of polling the job result, sec
211 :param verbose: ``bool``, print the job console updates during waiting
212 :param job_output_prefix: ``str``, print the prefix for each console
213 output line, with the pre-defined
214 substitution keys:
215 - '{name}' : the current job name
216 - '{build_id}' : the current build-id
217 - '{time}' : the current time
218 :returns: requests object with headers and console output, ``obj``
219 '''
Dennis Dmitriev61115112018-05-31 06:48:43 +0300220 start = [0]
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300221 time_str = time.strftime("%H:%M:%S")
222 prefix = "\n" + job_output_prefix.format(job_name=name,
223 build_number=build_id,
224 time=time_str)
225 if verbose:
226 print(prefix, end='')
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200227
228 def building():
Dmitry Tyzhnenko80ce0202019-02-07 13:27:19 +0200229 try:
Hanna Arhipova6d788142021-03-12 11:46:47 +0200230 # Nested retry decorator. Need to wait >30 min
231 # During mcp-upgrade job the Jenkins can being upgrading
232 # and can be inaccessible for >20 min
233 status = not retry(max_count=30)(
234 self.build_info)(name, build_id)['building']
Dmitry Tyzhnenko80ce0202019-02-07 13:27:19 +0200235 except ConnectionError:
236 status = False
237
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300238 if verbose:
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300239 time_str = time.strftime("%H:%M:%S")
240 prefix = "\n" + job_output_prefix.format(
241 job_name=name, build_number=build_id, time=time_str)
Dennis Dmitriev61115112018-05-31 06:48:43 +0300242 res = self.get_progressive_build_output(name,
243 build_id,
244 start=start[0])
245 if 'X-Text-Size' in res.headers:
246 text_size = int(res.headers['X-Text-Size'])
247 if start[0] < text_size:
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300248 text = res.content.decode('utf-8',
249 errors='backslashreplace')
250 print(text.replace("\n", prefix), end='')
Dennis Dmitriev61115112018-05-31 06:48:43 +0300251 start[0] = text_size
252 return status
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200253
254 helpers.wait(
255 building,
256 timeout=timeout,
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300257 interval=interval,
Dennis Dmitriev13e804b2018-10-09 19:25:14 +0300258 timeout_msg=('Timeout waiting the job {0}:{1} in {2} sec.'
259 .format(name, build_id, timeout)))
Dmitry Tyzhnenkob610afd2018-02-19 15:43:45 +0200260
Hanna Arhipovab2522692020-09-23 15:25:11 +0300261 @retry()
Dmitry Tyzhnenkob610afd2018-02-19 15:43:45 +0200262 def get_build_output(self, name, build_id):
Anna Arhipova839a55f2023-06-19 10:48:00 +0200263 output = self.__client.get_build_console_output(name, build_id)
264 # Clean output from any info added by addons
265 result = ''
266 for line in output.splitlines():
267 result += re.sub(r'<span.+</span>', '', line)
268 return result
Dennis Dmitriev61115112018-05-31 06:48:43 +0300269
Hanna Arhipova84aeab32021-05-24 13:41:00 +0300270 @retry(max_count=20, sleep_before_retry=30)
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300271 def get_progressive_build_output(self, name, build_id, start=0):
Dennis Dmitriev61115112018-05-31 06:48:43 +0300272 '''Get build console text.
273
274 :param name: Job name, ``str``
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300275 :param build_id: Build id, ``int``
276 :param start: Start offset, ``int``
Dennis Dmitriev61115112018-05-31 06:48:43 +0300277 :returns: requests object with headers and console output, ``obj``
278 '''
279 folder_url, short_name = self.__client._get_job_folder(name)
280
281 PROGRESSIVE_CONSOLE_OUTPUT = (
282 '%(folder_url)sjob/%(short_name)s/%(build_id)d/'
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300283 'logText/progressiveText?start=%(start)d')
Hanna Arhipovae1f0b472020-01-03 17:38:11 +0200284 req = send_request(
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300285 'GET',
286 self.__client._build_url(PROGRESSIVE_CONSOLE_OUTPUT, locals()))
Pavel Glazov794b1a92022-10-03 16:33:04 +0400287 return (self.__client.jenkins_request(req))
Dennis Dmitriev27a96792018-07-30 07:52:03 +0300288
Hanna Arhipovab2522692020-09-23 15:25:11 +0300289 @retry(max_count=12)
Dennis Dmitriev27a96792018-07-30 07:52:03 +0300290 def get_workflow(self, name, build_id, enode=None, mode='describe'):
291 '''Get workflow results from pipeline job
292
293 :param name: job name
294 :param build_id: str, build number or 'lastBuild'
295 :param enode: int, execution node in the workflow
296 :param mode: the stage or execution node description if 'describe',
297 the execution node log if 'log'
298 '''
299 folder_url, short_name = self.__client._get_job_folder(name)
300
301 if enode:
302 WORKFLOW_DESCRIPTION = (
303 '%(folder_url)sjob/%(short_name)s/%(build_id)s/'
304 'execution/node/%(enode)d/wfapi/%(mode)s')
305 else:
306 WORKFLOW_DESCRIPTION = (
307 '%(folder_url)sjob/%(short_name)s/%(build_id)s/wfapi/%(mode)s')
Hanna Arhipovae1f0b472020-01-03 17:38:11 +0200308 req = send_request(
Dennis Dmitriev27a96792018-07-30 07:52:03 +0300309 'GET',
310 self.__client._build_url(WORKFLOW_DESCRIPTION, locals()))
311 response = self.__client.jenkins_open(req)
312 return json.loads(response)
Dennis Dmitriev8565c342019-02-11 23:45:03 +0200313
Hanna Arhipovab2522692020-09-23 15:25:11 +0300314 @retry(max_count=12)
Dennis Dmitriev8565c342019-02-11 23:45:03 +0200315 def get_artifact(self, name, build_id, artifact_path, destination_name):
316 '''Wait until the specified build is finished
317
318 :param name: ``str``, job name
319 :param build_id: ``str``, build id or "lastBuild"
320 :param artifact_path: ``str``, path and filename of the artifact
321 relative to the job URL
322 :param artifact_path: ``str``, destination path and filename
323 on the local filesystem where to save
324 the artifact content
325 :returns: requests object with headers and console output, ``obj``
326 '''
327 folder_url, short_name = self.__client._get_job_folder(name)
328
329 DOWNLOAD_URL = ('%(folder_url)sjob/%(short_name)s/%(build_id)s/'
330 'artifact/%(artifact_path)s')
Hanna Arhipovae1f0b472020-01-03 17:38:11 +0200331 req = send_request(
Dennis Dmitriev8565c342019-02-11 23:45:03 +0200332 'GET',
333 self.__client._build_url(DOWNLOAD_URL, locals()))
334
335 response = self.__client.jenkins_request(req)
336 return response.content