blob: eb6707460dad4d2b0ca091f7e7cf18e32e79b1d8 [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
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +02009
10from devops.helpers import helpers
11
Dmitry Tyzhnenko80ce0202019-02-07 13:27:19 +020012from requests.exceptions import ConnectionError
13
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020014
Hanna Arhipovab2522692020-09-23 15:25:11 +030015def retry(max_count=6,
16 sleep_before_retry=10):
17 def _retry(func):
18 def __retry(*args, **kwargs):
19 """
20 Waits some time and retries the requests if it fails with
21 with any error
22 Raises Exceptions after all unsuccessful tries
23
24 :param func: callable
25 :param args, kwargs: parameters of decorated functions
26 :param max_count: times of retries
27 :param sleep_before_retry: how many seconds needs to wait before
28 the next retry
29 :return: response
30 :raise ConnectionError after several unsuccessful connections
31 """
Hanna Arhipova6d788142021-03-12 11:46:47 +020032 err_msg = None
Hanna Arhipovab2522692020-09-23 15:25:11 +030033 for count in range(max_count):
34 try:
35 return func(*args, **kwargs)
36 except Exception as err:
Hanna Arhipova6d788142021-03-12 11:46:47 +020037 err_msg = err
38 print("Try {count}/{max_count} caught "
39 "Exception in {fn}: {err}."
40 "\n... repeat after {secs} secs".
Hanna Arhipovab2522692020-09-23 15:25:11 +030041 format(err=err,
42 count=count+1,
43 max_count=max_count,
Hanna Arhipova6d788142021-03-12 11:46:47 +020044 secs=sleep_before_retry,
45 fn=func.__name__))
Hanna Arhipovab2522692020-09-23 15:25:11 +030046 time.sleep(sleep_before_retry)
47 print("Function failed in {total_time} seconds".format(
48 total_time=max_count*sleep_before_retry)
49 )
Hanna Arhipova6d788142021-03-12 11:46:47 +020050 raise err_msg
Hanna Arhipovab2522692020-09-23 15:25:11 +030051 return __retry
52 return _retry
53
54
55@retry(max_count=5, sleep_before_retry=2)
Hanna Arhipovae1f0b472020-01-03 17:38:11 +020056def send_request(action, endpoint):
57 """
Hanna Arhipovab2522692020-09-23 15:25:11 +030058 Makes request with described operation to endpoint
59 :param action: string, type of operation GET, POST, DELETE and so on
60 :param endpoint: string, url to send request
61 :return: response
Hanna Arhipovae1f0b472020-01-03 17:38:11 +020062 """
Hanna Arhipovab2522692020-09-23 15:25:11 +030063 response = requests.Request(action, endpoint)
64 return response
Hanna Arhipovae1f0b472020-01-03 17:38:11 +020065
66
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020067class JenkinsClient(object):
68
Dennis Dmitriev27a96792018-07-30 07:52:03 +030069 def __init__(self, host=None, username='admin', password='r00tme'):
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020070 host = host or 'http://172.16.44.33:8081'
obutenkoca858402019-07-04 18:31:39 +030071 self.__client = jenkins.Jenkins(
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020072 host,
73 username=username,
74 password=password)
obutenkoca858402019-07-04 18:31:39 +030075 self.__client._session.verify = False
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020076
Hanna Arhipovab2522692020-09-23 15:25:11 +030077 @retry()
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020078 def jobs(self):
79 return self.__client.get_jobs()
80
81 def find_jobs(self, name):
82 return filter(lambda x: name in x['fullname'], self.jobs())
83
Hanna Arhipovab2522692020-09-23 15:25:11 +030084 @retry()
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020085 def job_info(self, name):
Hanna Arhipovab2522692020-09-23 15:25:11 +030086 return self.__client.get_job_info(name)
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020087
88 def list_builds(self, name):
89 return self.job_info(name).get('builds')
90
Hanna Arhipovab2522692020-09-23 15:25:11 +030091 @retry()
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020092 def build_info(self, name, build_id):
Hanna Arhipovab2522692020-09-23 15:25:11 +030093 return self.__client.get_build_info(name, build_id)
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020094
95 def job_params(self, name):
96 job = self.job_info(name)
97 job_params = next(
98 p for p in job['property'] if
99 'hudson.model.ParametersDefinitionProperty' == p['_class'])
100 job_params = job_params['parameterDefinitions']
101 return job_params
102
Hanna Arhipova874c68f2021-03-29 15:57:19 +0300103 def make_defaults_params(self, name):
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200104 job_params = self.job_params(name)
105 def_params = dict(
106 [(j['name'], j['defaultParameterValue']['value'])
107 for j in job_params])
108 return def_params
109
Hanna Arhipova874c68f2021-03-29 15:57:19 +0300110 def _correct_yaml_params(self, job_name, params):
111 """
112 Params can be defined as a nested dict.
113 In that case 2nd-layer dict will be translated to YAML text and
114 added to default parameter value
115
116 :param job_name: Job name
117 :param params: dict of JenkinsJobs parameters
118 :return: nothing
119 """
120 for param_name, param_value in params.items():
121 if not isinstance(param_value, dict):
122 continue
123 default_param = self.make_defaults_params(job_name).get(param_name)
124 if default_param is None:
125 print("{param} param of {job} job doesn't exist. "
126 "Ignoring enriching it with {value}".format(
127 param=param_name,
128 job=job_name,
129 value=param_value
130 ))
131 continue
132 yaml_param = yaml.load(default_param)
133 yaml_param.update(param_value)
134 params[param_name] = yaml.dump(yaml_param,
135 default_flow_style=False)
136 return params
137
Hanna Arhipovab2522692020-09-23 15:25:11 +0300138 @retry()
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300139 def run_build(self, name, params=None, timeout=600, verbose=False):
Hanna Arhipova874c68f2021-03-29 15:57:19 +0300140 params = params or self.make_defaults_params(name)
141 params = self._correct_yaml_params(job_name=name,
142 params=params)
143
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300144 num = self.__client.build_job(name, params)
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300145 time.sleep(2) # wait while job is started
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300146
Dmitry Tyzhnenkob39de052019-03-21 17:05:07 +0200147 def is_build_queued():
148 try:
149 item = self.__client.get_queue_item(num)
150 ts = item['inQueueSince'] / 1000
151 since_time = datetime.datetime.fromtimestamp(ts)
152 print("Build in the queue since {}".format(since_time))
153 return True
154 except jenkins.JenkinsException:
155 if verbose:
156 print("Build have not been queued {} yet".format(num))
157
158 helpers.wait(
159 is_build_queued,
160 timeout=timeout,
161 interval=30,
162 timeout_msg='Timeout waiting to queue the build '
163 'for {} job'.format(name))
164
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300165 def is_blocked():
166 queued = self.__client.get_queue_item(num)
167 status = not queued['blocked']
168 if not status and verbose:
169 print("pending the job [{}] : {}".format(name, queued['why']))
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300170 return (status and
171 'executable' in (queued or {}) and
172 'number' in (queued['executable'] or {}))
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300173
174 helpers.wait(
175 is_blocked,
176 timeout=timeout,
177 interval=30,
178 timeout_msg='Timeout waiting to run the job [{}]'.format(name))
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300179 build_id = self.__client.get_queue_item(num)['executable']['number']
Dmitry Tyzhnenkob39de052019-03-21 17:05:07 +0200180
181 def is_build_started():
182 try:
183 build = self.__client.get_build_info(name, build_id)
184 ts = float(build['timestamp']) / 1000
185 start_time = datetime.datetime.fromtimestamp(ts)
186 print("the build {} in {} have started at {} UTC".format(
187 build_id, name, start_time))
188 return True
189 except jenkins.JenkinsException:
190 if verbose:
191 print("the build {} in {} have not strated yet".format(
192 build_id, name))
193 helpers.wait(
194 is_build_started,
195 timeout=timeout,
196 interval=30,
197 timeout_msg='Timeout waiting to run build of '
198 'the job [{}]'.format(name))
199
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200200 return name, build_id
201
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300202 def wait_end_of_build(self, name, build_id, timeout=600, interval=5,
203 verbose=False, job_output_prefix=''):
204 '''Wait until the specified build is finished
205
206 :param name: ``str``, job name
207 :param build_id: ``int``, build id
208 :param timeout: ``int``, timeout waiting the job, sec
209 :param interval: ``int``, interval of polling the job result, sec
210 :param verbose: ``bool``, print the job console updates during waiting
211 :param job_output_prefix: ``str``, print the prefix for each console
212 output line, with the pre-defined
213 substitution keys:
214 - '{name}' : the current job name
215 - '{build_id}' : the current build-id
216 - '{time}' : the current time
217 :returns: requests object with headers and console output, ``obj``
218 '''
Dennis Dmitriev61115112018-05-31 06:48:43 +0300219 start = [0]
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300220 time_str = time.strftime("%H:%M:%S")
221 prefix = "\n" + job_output_prefix.format(job_name=name,
222 build_number=build_id,
223 time=time_str)
224 if verbose:
225 print(prefix, end='')
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200226
227 def building():
Dmitry Tyzhnenko80ce0202019-02-07 13:27:19 +0200228 try:
Hanna Arhipova6d788142021-03-12 11:46:47 +0200229 # Nested retry decorator. Need to wait >30 min
230 # During mcp-upgrade job the Jenkins can being upgrading
231 # and can be inaccessible for >20 min
232 status = not retry(max_count=30)(
233 self.build_info)(name, build_id)['building']
Dmitry Tyzhnenko80ce0202019-02-07 13:27:19 +0200234 except ConnectionError:
235 status = False
236
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300237 if verbose:
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300238 time_str = time.strftime("%H:%M:%S")
239 prefix = "\n" + job_output_prefix.format(
240 job_name=name, build_number=build_id, time=time_str)
Dennis Dmitriev61115112018-05-31 06:48:43 +0300241 res = self.get_progressive_build_output(name,
242 build_id,
243 start=start[0])
244 if 'X-Text-Size' in res.headers:
245 text_size = int(res.headers['X-Text-Size'])
246 if start[0] < text_size:
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300247 text = res.content.decode('utf-8',
248 errors='backslashreplace')
249 print(text.replace("\n", prefix), end='')
Dennis Dmitriev61115112018-05-31 06:48:43 +0300250 start[0] = text_size
251 return status
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200252
253 helpers.wait(
254 building,
255 timeout=timeout,
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300256 interval=interval,
Dennis Dmitriev13e804b2018-10-09 19:25:14 +0300257 timeout_msg=('Timeout waiting the job {0}:{1} in {2} sec.'
258 .format(name, build_id, timeout)))
Dmitry Tyzhnenkob610afd2018-02-19 15:43:45 +0200259
Hanna Arhipovab2522692020-09-23 15:25:11 +0300260 @retry()
Dmitry Tyzhnenkob610afd2018-02-19 15:43:45 +0200261 def get_build_output(self, name, build_id):
262 return self.__client.get_build_console_output(name, build_id)
Dennis Dmitriev61115112018-05-31 06:48:43 +0300263
Hanna Arhipova84aeab32021-05-24 13:41:00 +0300264 @retry(max_count=20, sleep_before_retry=30)
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300265 def get_progressive_build_output(self, name, build_id, start=0):
Dennis Dmitriev61115112018-05-31 06:48:43 +0300266 '''Get build console text.
267
268 :param name: Job name, ``str``
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300269 :param build_id: Build id, ``int``
270 :param start: Start offset, ``int``
Dennis Dmitriev61115112018-05-31 06:48:43 +0300271 :returns: requests object with headers and console output, ``obj``
272 '''
273 folder_url, short_name = self.__client._get_job_folder(name)
274
275 PROGRESSIVE_CONSOLE_OUTPUT = (
276 '%(folder_url)sjob/%(short_name)s/%(build_id)d/'
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300277 'logText/progressiveText?start=%(start)d')
Hanna Arhipovae1f0b472020-01-03 17:38:11 +0200278 req = send_request(
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300279 'GET',
280 self.__client._build_url(PROGRESSIVE_CONSOLE_OUTPUT, locals()))
281 return(self.__client.jenkins_request(req))
Dennis Dmitriev27a96792018-07-30 07:52:03 +0300282
Hanna Arhipovab2522692020-09-23 15:25:11 +0300283 @retry(max_count=12)
Dennis Dmitriev27a96792018-07-30 07:52:03 +0300284 def get_workflow(self, name, build_id, enode=None, mode='describe'):
285 '''Get workflow results from pipeline job
286
287 :param name: job name
288 :param build_id: str, build number or 'lastBuild'
289 :param enode: int, execution node in the workflow
290 :param mode: the stage or execution node description if 'describe',
291 the execution node log if 'log'
292 '''
293 folder_url, short_name = self.__client._get_job_folder(name)
294
295 if enode:
296 WORKFLOW_DESCRIPTION = (
297 '%(folder_url)sjob/%(short_name)s/%(build_id)s/'
298 'execution/node/%(enode)d/wfapi/%(mode)s')
299 else:
300 WORKFLOW_DESCRIPTION = (
301 '%(folder_url)sjob/%(short_name)s/%(build_id)s/wfapi/%(mode)s')
Hanna Arhipovae1f0b472020-01-03 17:38:11 +0200302 req = send_request(
Dennis Dmitriev27a96792018-07-30 07:52:03 +0300303 'GET',
304 self.__client._build_url(WORKFLOW_DESCRIPTION, locals()))
305 response = self.__client.jenkins_open(req)
306 return json.loads(response)
Dennis Dmitriev8565c342019-02-11 23:45:03 +0200307
Hanna Arhipovab2522692020-09-23 15:25:11 +0300308 @retry(max_count=12)
Dennis Dmitriev8565c342019-02-11 23:45:03 +0200309 def get_artifact(self, name, build_id, artifact_path, destination_name):
310 '''Wait until the specified build is finished
311
312 :param name: ``str``, job name
313 :param build_id: ``str``, build id or "lastBuild"
314 :param artifact_path: ``str``, path and filename of the artifact
315 relative to the job URL
316 :param artifact_path: ``str``, destination path and filename
317 on the local filesystem where to save
318 the artifact content
319 :returns: requests object with headers and console output, ``obj``
320 '''
321 folder_url, short_name = self.__client._get_job_folder(name)
322
323 DOWNLOAD_URL = ('%(folder_url)sjob/%(short_name)s/%(build_id)s/'
324 'artifact/%(artifact_path)s')
Hanna Arhipovae1f0b472020-01-03 17:38:11 +0200325 req = send_request(
Dennis Dmitriev8565c342019-02-11 23:45:03 +0200326 'GET',
327 self.__client._build_url(DOWNLOAD_URL, locals()))
328
329 response = self.__client.jenkins_request(req)
330 return response.content