Skip to content

Commit cc2fc1b

Browse files
committed
feat(ZNTA-2657): Script to get last successful builds Jenkins
1 parent 2b0b296 commit cc2fc1b

2 files changed

Lines changed: 445 additions & 0 deletions

File tree

JenkinsHelper.py

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
#!/usr/bin/env python
2+
"""Jenkins Helper functions
3+
It contains jenkins helper
4+
Run JenkinsHelper --help or JenkinsHelper --help <command> for
5+
detail help."""
6+
7+
from __future__ import (
8+
absolute_import, division, print_function, unicode_literals)
9+
import ast
10+
import logging
11+
import os
12+
import os.path
13+
import re
14+
import sys
15+
16+
try:
17+
# We need to import 'List' and 'Any' for mypy to work
18+
from typing import List, Any # noqa: F401 # pylint: disable=unused-import
19+
except ImportError:
20+
sys.stderr.write("python typing module is not installed" + os.linesep)
21+
22+
from ZanataFunctions import UrlHelper
23+
from ZanataArgParser import ZanataArgParser # pylint: disable=E0401
24+
25+
26+
class JenkinsServer(object):
27+
"""JenkinsServer can connect to a Jenkins server"""
28+
def __init__(self, server_url, user, token):
29+
# type: (str, str,str) -> None
30+
self.url_helper = UrlHelper(
31+
server_url, user, token)
32+
self.server_url = server_url
33+
self.user = user
34+
self.token = token
35+
36+
@classmethod
37+
def add_parser(cls, arg_parser=None, env_sub_commands=None):
38+
# type: (ZanataArgParser) -> ZanataArgParser
39+
"""Add JenkinsServer parameters to a parser"""
40+
if not arg_parser:
41+
arg_parser = ZanataArgParser(description=__doc__)
42+
43+
# Add env
44+
arg_parser.add_env(
45+
'JENKINS_URL', dest='server_url', required=True,
46+
sub_commands=env_sub_commands)
47+
arg_parser.add_env(
48+
'ZANATA_JENKINS_USER', dest='user', required=True,
49+
sub_commands=env_sub_commands)
50+
arg_parser.add_env(
51+
'ZANATA_JENKINS_TOKEN', dest='token', required=True,
52+
sub_commands=env_sub_commands)
53+
return arg_parser
54+
55+
@classmethod
56+
def init_from_parsed_args(cls, args):
57+
"""New an instance from parsed args"""
58+
return cls(args.server_url, args.user, args.token)
59+
60+
61+
class JenkinsJob(object):
62+
"""JenkinsJob object can access a Jenkins Job"""
63+
64+
@staticmethod
65+
def dict_get_elem_by_path(dic, path):
66+
# type (dict, str) -> object
67+
"""Return the elem in python dictionary given path
68+
for example: you can use a/b to retrieve answer from following
69+
dict:
70+
{ 'a': { 'b': 'answer' }}"""
71+
obj = dic
72+
for key in path.split('/'):
73+
if obj[key]:
74+
obj = obj[key]
75+
else:
76+
return None
77+
return obj
78+
79+
@staticmethod
80+
def print_key_value(key, value):
81+
# type (str, str) -> None
82+
"""Pretty print the key and value"""
83+
return "%30s : %s" % (key, value)
84+
85+
def get_elem(self, path):
86+
# type: (str) -> object
87+
"""Get element from the job object"""
88+
return JenkinsJob.dict_get_elem_by_path(self.content, path)
89+
90+
def __repr__(self):
91+
# type: () -> str
92+
result = "\n".join([
93+
JenkinsJob.print_key_value(tup[0], tup[1]) for tup in [
94+
['job_name', self.job_name],
95+
['folder', self.folder],
96+
['branch', self.branch]]])
97+
if self.content:
98+
result += "\n\n%s" % "\n".join([
99+
JenkinsJob.print_key_value(
100+
key, self.get_elem(key)) for key in [
101+
'displayName',
102+
'fullName',
103+
'lastBuild/number',
104+
'lastCompletedBuild/number',
105+
'lastFailedBuild/number',
106+
'lastSuccessfulBuild/number']])
107+
return result
108+
109+
def __init__(self, server, job_name, folder='', branch=''):
110+
# type (JenkinsServer, str, str, str) -> None
111+
self.server = server
112+
self.job_name = job_name
113+
self.folder = folder
114+
self.branch = branch
115+
self.content = None
116+
job_path = "job/%s" % self.job_name
117+
if folder:
118+
job_path = "job/%s/%s" % (folder, job_path)
119+
if branch:
120+
job_path += "/job/%s" % branch
121+
self.url = "%s%s" % (self.server.server_url, job_path)
122+
123+
@classmethod
124+
def add_parser(
125+
cls, arg_parser=None,
126+
only_options=False, env_sub_commands=None):
127+
# type: (ZanataArgParser, bool) -> ZanataArgParser
128+
"""Add JenkinsJob parameters to parser
129+
arg_parser: existing parser to be appended to
130+
only_options: Add only options and JenkinsServer env"""
131+
if not arg_parser or not arg_parser.has_env('JENKINS_URL'):
132+
arg_parser = JenkinsServer.add_parser(
133+
arg_parser, env_sub_commands)
134+
arg_parser.add_common_argument(
135+
'-b', '--branch', type=str,
136+
help='branch or PR name')
137+
arg_parser.add_common_argument(
138+
'-F', '--folder', type=str,
139+
help='GitHub Organization Folder')
140+
if not only_options:
141+
arg_parser.add_common_argument(
142+
'job_name', type=str, help='job name')
143+
144+
# Add sub commands
145+
arg_parser.add_sub_command(
146+
'get-job', None,
147+
help='Show job objects')
148+
arg_parser.add_sub_command(
149+
'get-last-successful-build', None,
150+
help=cls.get_last_successful_build.__doc__)
151+
arg_parser.add_sub_command(
152+
'get-last-successful-artifacts',
153+
{
154+
'-p --artifact-path-patterns': {
155+
'type': str, 'default': '.*',
156+
'help': 'comma split artifact path regex pattern'}}, # noqa: E501, #pylint: disable=line-too-long
157+
help='Get matching last-successful artifacts. Default: .*')
158+
arg_parser.add_sub_command(
159+
'download-last-successful-artifacts',
160+
{
161+
'-p --artifact-path-patterns': {
162+
'type': str, 'default': '.*',
163+
'help': 'comma split artifact path regex'
164+
},
165+
'-d --download-dir': {
166+
'type': str, 'default': '.',
167+
'help': 'Download directory'}
168+
},
169+
help='Get matching last-successful artifacts. Default: .*')
170+
return arg_parser
171+
172+
@classmethod
173+
def init_from_parsed_args(cls, args):
174+
"""New an instance from parsed args"""
175+
server = JenkinsServer.init_from_parsed_args(args)
176+
kwargs = {'job_name': args.job_name}
177+
for k in ['folder', 'branch']:
178+
if hasattr(args, k):
179+
kwargs[k] = getattr(args, k)
180+
return cls(server, **kwargs)
181+
182+
def load(self):
183+
# type: () -> None
184+
"""Load the build object from Jenkins server"""
185+
logging.debug("Loading job from %s/api/python", self.url)
186+
self.content = ast.literal_eval(UrlHelper.read(
187+
"%s/api/python" % self.url))
188+
189+
def get_last_successful_build(self):
190+
# type: () -> JenkinsJobBuild
191+
"""Get last successful build"""
192+
if not self.content:
193+
self.load()
194+
195+
if not self.content:
196+
raise AssertionError("Failed to load job from %s" % self.url)
197+
return JenkinsJobBuild(
198+
self,
199+
int(self.get_elem('lastSuccessfulBuild/number')),
200+
self.get_elem('lastSuccessfulBuild/url'))
201+
202+
def get_last_successful_artifacts(
203+
self, artifact_path_patterns=None):
204+
# type: (List[str]) -> List[str]
205+
"""Get last successful artifacts that matches patterns"""
206+
build = self.get_last_successful_build()
207+
return build.list_artifacts_related_paths(artifact_path_patterns)
208+
209+
def download_last_successful_artifacts(
210+
self, artifact_path_patterns=None, download_dir='.'):
211+
# type: (List[str])-> List[str]
212+
"""Download last successful artifacts that matches patterns.
213+
Returns related path of artifacts
214+
215+
Note the directory structure will be flattern."""
216+
if not artifact_path_patterns:
217+
artifact_path_patterns = ['.*']
218+
build = self.get_last_successful_build()
219+
artifact_path_list = build.list_artifacts_related_paths(
220+
artifact_path_patterns)
221+
for artifact_path in artifact_path_list:
222+
UrlHelper.download_file(
223+
build.url + 'artifact/' + artifact_path,
224+
download_dir=download_dir)
225+
return artifact_path_list
226+
227+
228+
class JenkinsJobBuild(object):
229+
"""Build object for Jenkins job"""
230+
231+
def __init__(self, parent_job, build_number, build_url):
232+
# type (object, int, str) -> None
233+
self.parent_job = parent_job
234+
self.number = build_number
235+
self.url = build_url
236+
self.content = None
237+
238+
def get_elem(self, path):
239+
# type: (str) -> object
240+
"""Get element from the build object"""
241+
return JenkinsJob.dict_get_elem_by_path(self.content, path)
242+
243+
def load(self):
244+
"""Load the build object from Jenkins server"""
245+
logging.debug("Loading build from %sapi/python", self.url)
246+
self.content = ast.literal_eval(UrlHelper.read(
247+
"%s/api/python" % self.url))
248+
249+
def list_artifacts_related_paths(self, artifact_path_patterns=None):
250+
# type: (str) -> List[str]
251+
"""Return a List of relativePaths of artifacts
252+
that matches the path patterns"""
253+
if not artifact_path_patterns:
254+
artifact_path_patterns = ['.*']
255+
if not self.content:
256+
self.load()
257+
if not self.content:
258+
raise AssertionError("Failed to load build from %s" % self.url)
259+
result = []
260+
for artifact in self.content['artifacts']:
261+
for pattern in artifact_path_patterns:
262+
if re.search(pattern, artifact['relativePath']):
263+
result.append(artifact['relativePath'])
264+
break # Only append once
265+
return result
266+
267+
def __repr__(self):
268+
# type: () -> str
269+
result = "\n".join([
270+
JenkinsJob.print_key_value(
271+
tup[0], str(tup[1])) for tup in [
272+
['number', self.number],
273+
['url', self.url]]])
274+
275+
if self.content:
276+
result += "\n\n%s" % "\n".join([
277+
JenkinsJob.print_key_value(
278+
key, self.get_elem(key)) for key in [
279+
'nextBuild/number',
280+
'previousBuild/number']])
281+
result += "\n\nArtifacts:\n%s" % "\n ".join(
282+
self.list_artifacts_related_paths())
283+
return result
284+
285+
286+
def run_sub_command(args):
287+
# type (ZanataArgParser.Namespace) -> None
288+
"""Run the sub command"""
289+
job = JenkinsJob.init_from_parsed_args(args)
290+
job.load()
291+
if args.sub_command == 'get-job':
292+
print(job)
293+
elif args.sub_command == 'get-last-successful-build':
294+
build = job.get_last_successful_build()
295+
build.load()
296+
print(build)
297+
elif args.sub_command == 'get-last-successful-artifacts':
298+
print('\n'.join(
299+
job.get_last_successful_artifacts(
300+
args.artifact_path_patterns.split(','))
301+
))
302+
elif args.sub_command == 'download-last-successful-artifacts':
303+
artifact_path_list = job.download_last_successful_artifacts(
304+
args.artifact_path_patterns.split(','),
305+
args.download_dir)
306+
print("Downloaded files %s" % '\n'.join(artifact_path_list))
307+
308+
309+
if __name__ == '__main__':
310+
run_sub_command(JenkinsJob.add_parser().parse_all())

0 commit comments

Comments
 (0)