blob: 39c698e793ec0bd51873da8032cddf0e9d46b2c8 [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
Dennis Dmitriev61115112018-05-31 06:48:43 +03007import requests
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +02008
9from devops.helpers import helpers
10
Dmitry Tyzhnenko80ce0202019-02-07 13:27:19 +020011from requests.exceptions import ConnectionError
12
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020013
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +030014class JenkinsWrapper(jenkins.Jenkins):
15 """Workaround for the bug:
16 https://bugs.launchpad.net/python-jenkins/+bug/1775047
17 """
18 def _response_handler(self, response):
19 '''Handle response objects'''
20
21 # raise exceptions if occurred
22 response.raise_for_status()
23
24 headers = response.headers
25 if (headers.get('content-length') is None and
26 headers.get('transfer-encoding') is None and
27 (response.status_code == 201 and
28 headers.get('location') is None) and
29 (response.content is None or len(response.content) <= 0)):
30 # response body should only exist if one of these is provided
31 raise jenkins.EmptyResponseException(
32 "Error communicating with server[%s]: "
33 "empty response" % self.server)
34
35 # Response objects will automatically return unicode encoded
36 # when accessing .text property
37 return response
38
39
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020040class JenkinsClient(object):
41
Dennis Dmitriev27a96792018-07-30 07:52:03 +030042 def __init__(self, host=None, username='admin', password='r00tme'):
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020043 host = host or 'http://172.16.44.33:8081'
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +030044 # self.__client = jenkins.Jenkins(
45 self.__client = JenkinsWrapper(
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020046 host,
47 username=username,
48 password=password)
49
50 def jobs(self):
51 return self.__client.get_jobs()
52
53 def find_jobs(self, name):
54 return filter(lambda x: name in x['fullname'], self.jobs())
55
56 def job_info(self, name):
57 return self.__client.get_job_info(name)
58
59 def list_builds(self, name):
60 return self.job_info(name).get('builds')
61
62 def build_info(self, name, build_id):
63 return self.__client.get_build_info(name, build_id)
64
65 def job_params(self, name):
66 job = self.job_info(name)
67 job_params = next(
68 p for p in job['property'] if
69 'hudson.model.ParametersDefinitionProperty' == p['_class'])
70 job_params = job_params['parameterDefinitions']
71 return job_params
72
73 def make_defults_params(self, name):
74 job_params = self.job_params(name)
75 def_params = dict(
76 [(j['name'], j['defaultParameterValue']['value'])
77 for j in job_params])
78 return def_params
79
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +030080 def run_build(self, name, params=None, timeout=600, verbose=False):
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020081 params = params or self.make_defults_params(name)
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +030082 num = self.__client.build_job(name, params)
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +030083 time.sleep(2) # wait while job is started
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +030084
Dmitry Tyzhnenkob39de052019-03-21 17:05:07 +020085 def is_build_queued():
86 try:
87 item = self.__client.get_queue_item(num)
88 ts = item['inQueueSince'] / 1000
89 since_time = datetime.datetime.fromtimestamp(ts)
90 print("Build in the queue since {}".format(since_time))
91 return True
92 except jenkins.JenkinsException:
93 if verbose:
94 print("Build have not been queued {} yet".format(num))
95
96 helpers.wait(
97 is_build_queued,
98 timeout=timeout,
99 interval=30,
100 timeout_msg='Timeout waiting to queue the build '
101 'for {} job'.format(name))
102
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300103 def is_blocked():
104 queued = self.__client.get_queue_item(num)
105 status = not queued['blocked']
106 if not status and verbose:
107 print("pending the job [{}] : {}".format(name, queued['why']))
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300108 return (status and
109 'executable' in (queued or {}) and
110 'number' in (queued['executable'] or {}))
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300111
112 helpers.wait(
113 is_blocked,
114 timeout=timeout,
115 interval=30,
116 timeout_msg='Timeout waiting to run the job [{}]'.format(name))
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300117 build_id = self.__client.get_queue_item(num)['executable']['number']
Dmitry Tyzhnenkob39de052019-03-21 17:05:07 +0200118
119 def is_build_started():
120 try:
121 build = self.__client.get_build_info(name, build_id)
122 ts = float(build['timestamp']) / 1000
123 start_time = datetime.datetime.fromtimestamp(ts)
124 print("the build {} in {} have started at {} UTC".format(
125 build_id, name, start_time))
126 return True
127 except jenkins.JenkinsException:
128 if verbose:
129 print("the build {} in {} have not strated yet".format(
130 build_id, name))
131 helpers.wait(
132 is_build_started,
133 timeout=timeout,
134 interval=30,
135 timeout_msg='Timeout waiting to run build of '
136 'the job [{}]'.format(name))
137
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200138 return name, build_id
139
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300140 def wait_end_of_build(self, name, build_id, timeout=600, interval=5,
141 verbose=False, job_output_prefix=''):
142 '''Wait until the specified build is finished
143
144 :param name: ``str``, job name
145 :param build_id: ``int``, build id
146 :param timeout: ``int``, timeout waiting the job, sec
147 :param interval: ``int``, interval of polling the job result, sec
148 :param verbose: ``bool``, print the job console updates during waiting
149 :param job_output_prefix: ``str``, print the prefix for each console
150 output line, with the pre-defined
151 substitution keys:
152 - '{name}' : the current job name
153 - '{build_id}' : the current build-id
154 - '{time}' : the current time
155 :returns: requests object with headers and console output, ``obj``
156 '''
Dennis Dmitriev61115112018-05-31 06:48:43 +0300157 start = [0]
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300158 time_str = time.strftime("%H:%M:%S")
159 prefix = "\n" + job_output_prefix.format(job_name=name,
160 build_number=build_id,
161 time=time_str)
162 if verbose:
163 print(prefix, end='')
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200164
165 def building():
Dmitry Tyzhnenko80ce0202019-02-07 13:27:19 +0200166 try:
167 status = not self.build_info(name, build_id)['building']
168 except ConnectionError:
169 status = False
170
Dennis Dmitrieva5bd1652018-05-31 20:57:19 +0300171 if verbose:
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300172 time_str = time.strftime("%H:%M:%S")
173 prefix = "\n" + job_output_prefix.format(
174 job_name=name, build_number=build_id, time=time_str)
Dennis Dmitriev61115112018-05-31 06:48:43 +0300175 res = self.get_progressive_build_output(name,
176 build_id,
177 start=start[0])
178 if 'X-Text-Size' in res.headers:
179 text_size = int(res.headers['X-Text-Size'])
180 if start[0] < text_size:
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300181 text = res.content.decode('utf-8',
182 errors='backslashreplace')
183 print(text.replace("\n", prefix), end='')
Dennis Dmitriev61115112018-05-31 06:48:43 +0300184 start[0] = text_size
185 return status
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200186
187 helpers.wait(
188 building,
189 timeout=timeout,
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300190 interval=interval,
Dennis Dmitriev13e804b2018-10-09 19:25:14 +0300191 timeout_msg=('Timeout waiting the job {0}:{1} in {2} sec.'
192 .format(name, build_id, timeout)))
Dmitry Tyzhnenkob610afd2018-02-19 15:43:45 +0200193
194 def get_build_output(self, name, build_id):
195 return self.__client.get_build_console_output(name, build_id)
Dennis Dmitriev61115112018-05-31 06:48:43 +0300196
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300197 def get_progressive_build_output(self, name, build_id, start=0):
Dennis Dmitriev61115112018-05-31 06:48:43 +0300198 '''Get build console text.
199
200 :param name: Job name, ``str``
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300201 :param build_id: Build id, ``int``
202 :param start: Start offset, ``int``
Dennis Dmitriev61115112018-05-31 06:48:43 +0300203 :returns: requests object with headers and console output, ``obj``
204 '''
205 folder_url, short_name = self.__client._get_job_folder(name)
206
207 PROGRESSIVE_CONSOLE_OUTPUT = (
208 '%(folder_url)sjob/%(short_name)s/%(build_id)d/'
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300209 'logText/progressiveText?start=%(start)d')
210 req = requests.Request(
211 'GET',
212 self.__client._build_url(PROGRESSIVE_CONSOLE_OUTPUT, locals()))
213 return(self.__client.jenkins_request(req))
Dennis Dmitriev27a96792018-07-30 07:52:03 +0300214
215 def get_workflow(self, name, build_id, enode=None, mode='describe'):
216 '''Get workflow results from pipeline job
217
218 :param name: job name
219 :param build_id: str, build number or 'lastBuild'
220 :param enode: int, execution node in the workflow
221 :param mode: the stage or execution node description if 'describe',
222 the execution node log if 'log'
223 '''
224 folder_url, short_name = self.__client._get_job_folder(name)
225
226 if enode:
227 WORKFLOW_DESCRIPTION = (
228 '%(folder_url)sjob/%(short_name)s/%(build_id)s/'
229 'execution/node/%(enode)d/wfapi/%(mode)s')
230 else:
231 WORKFLOW_DESCRIPTION = (
232 '%(folder_url)sjob/%(short_name)s/%(build_id)s/wfapi/%(mode)s')
233 req = requests.Request(
234 'GET',
235 self.__client._build_url(WORKFLOW_DESCRIPTION, locals()))
236 response = self.__client.jenkins_open(req)
237 return json.loads(response)
Dennis Dmitriev8565c342019-02-11 23:45:03 +0200238
239 def get_artifact(self, name, build_id, artifact_path, destination_name):
240 '''Wait until the specified build is finished
241
242 :param name: ``str``, job name
243 :param build_id: ``str``, build id or "lastBuild"
244 :param artifact_path: ``str``, path and filename of the artifact
245 relative to the job URL
246 :param artifact_path: ``str``, destination path and filename
247 on the local filesystem where to save
248 the artifact content
249 :returns: requests object with headers and console output, ``obj``
250 '''
251 folder_url, short_name = self.__client._get_job_folder(name)
252
253 DOWNLOAD_URL = ('%(folder_url)sjob/%(short_name)s/%(build_id)s/'
254 'artifact/%(artifact_path)s')
255 req = requests.Request(
256 'GET',
257 self.__client._build_url(DOWNLOAD_URL, locals()))
258
259 response = self.__client.jenkins_request(req)
260 return response.content