|
| 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 | + from typing import List, Any # noqa: F401 # pylint: disable=unused-import |
| 18 | +except ImportError: |
| 19 | + sys.stderr.write("python typing module is not installed" + os.linesep) |
| 20 | + |
| 21 | +from ZanataFunctions import UrlHelper |
| 22 | +from ZanataArgParser import ZanataArgParser |
| 23 | + |
| 24 | + |
| 25 | +try: |
| 26 | + # We need to import 'List' and 'Any' for mypy to work |
| 27 | + from typing import List, Any # noqa: E501,F401,F811 # pylint: disable=unused-import |
| 28 | +except ImportError: |
| 29 | + sys.stderr.write("python typing module is not installed" + os.linesep) |
| 30 | + |
| 31 | + |
| 32 | +class JenkinsServer(object): |
| 33 | + """JenkinsServer can connect to a Jenkins server""" |
| 34 | + def __init__(self, server_url, user, token): |
| 35 | + # type: (str, str,str) -> None |
| 36 | + self.url_helper = UrlHelper( |
| 37 | + server_url, user, token) |
| 38 | + self.server_url = server_url |
| 39 | + self.user = user |
| 40 | + self.token = token |
| 41 | + |
| 42 | + @classmethod |
| 43 | + def add_parser(cls, arg_parser=None): |
| 44 | + # type: (ZanataArgParser) -> ZanataArgParser |
| 45 | + """Add JenkinsServer parameters to a parser""" |
| 46 | + if not arg_parser: |
| 47 | + arg_parser = ZanataArgParser(description=__doc__) |
| 48 | + |
| 49 | + # Add env |
| 50 | + arg_parser.add_env('JENKINS_URL', dest='server_url', required=True) |
| 51 | + arg_parser.add_env('ZANATA_JENKINS_USER', dest='user', required=True) |
| 52 | + arg_parser.add_env('ZANATA_JENKINS_TOKEN', dest='token', required=True) |
| 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(cls, arg_parser=None, only_options=False): |
| 125 | + # type: (ZanataArgParser, bool) -> ZanataArgParser |
| 126 | + """Add JenkinsJob parameters to parser |
| 127 | + arg_parser: existing parser to be appended to |
| 128 | + only_options: Add only options and JenkinsServer env""" |
| 129 | + if not arg_parser or not arg_parser.has_env('JENKINS_URL'): |
| 130 | + arg_parser = JenkinsServer.add_parser(arg_parser) |
| 131 | + arg_parser.add_common_argument( |
| 132 | + '-b', '--branch', type=str, |
| 133 | + help='branch or PR name') |
| 134 | + arg_parser.add_common_argument( |
| 135 | + '-F', '--folder', type=str, |
| 136 | + help='GitHub Organization Folder') |
| 137 | + if not only_options: |
| 138 | + arg_parser.add_common_argument( |
| 139 | + 'job_name', type=str, help='job name') |
| 140 | + |
| 141 | + # Add sub commands |
| 142 | + arg_parser.add_sub_command( |
| 143 | + 'get-job', None, |
| 144 | + help='Show job objects') |
| 145 | + arg_parser.add_sub_command( |
| 146 | + 'get-last-successful-build', None, |
| 147 | + help=cls.get_last_successful_build.__doc__) |
| 148 | + arg_parser.add_sub_command( |
| 149 | + 'get-last-successful-artifacts', |
| 150 | + { |
| 151 | + '-p --artifact-path-patterns': { |
| 152 | + 'type': str, 'default': '.*', |
| 153 | + 'help': 'comma split artifact path regex pattern'}}, # noqa: E501, #pylint: disable=line-too-long |
| 154 | + help='Get matching last-successful artifacts. Default: .*') |
| 155 | + arg_parser.add_sub_command( |
| 156 | + 'download-last-successful-artifacts', |
| 157 | + { |
| 158 | + '-p --artifact-path-patterns': { |
| 159 | + 'type': str, 'default': '.*', |
| 160 | + 'help': 'comma split artifact path regex' |
| 161 | + }, |
| 162 | + '-d --download-dir': { |
| 163 | + 'type': str, 'default': '.', |
| 164 | + 'help': 'Download directory'} |
| 165 | + }, |
| 166 | + help='Get matching last-successful artifacts. Default: .*') |
| 167 | + return arg_parser |
| 168 | + |
| 169 | + @classmethod |
| 170 | + def init_from_parsed_args(cls, args): |
| 171 | + """New an instance from parsed args""" |
| 172 | + server = JenkinsServer.init_from_parsed_args(args) |
| 173 | + kwargs = {'job_name': args.job_name} |
| 174 | + for k in ['folder', 'branch']: |
| 175 | + if hasattr(args, k): |
| 176 | + kwargs[k] = getattr(args, k) |
| 177 | + return cls(server, **kwargs) |
| 178 | + |
| 179 | + def load(self): |
| 180 | + # type: () -> None |
| 181 | + """Load the build object from Jenkins server""" |
| 182 | + logging.debug("Loading job from %s/api/python", self.url) |
| 183 | + self.content = ast.literal_eval(UrlHelper.read( |
| 184 | + "%s/api/python" % self.url)) |
| 185 | + |
| 186 | + def get_last_successful_build(self): |
| 187 | + # type: () -> JenkinsJobBuild |
| 188 | + """Get last successful build""" |
| 189 | + if not self.content: |
| 190 | + self.load() |
| 191 | + |
| 192 | + if not self.content: |
| 193 | + raise AssertionError("Failed to load job from %s" % self.url) |
| 194 | + return JenkinsJobBuild( |
| 195 | + self, |
| 196 | + int(self.get_elem('lastSuccessfulBuild/number')), |
| 197 | + self.get_elem('lastSuccessfulBuild/url')) |
| 198 | + |
| 199 | + def get_last_successful_artifacts( |
| 200 | + self, artifact_path_patterns=None): |
| 201 | + # type: (List[str]) -> List[str] |
| 202 | + """Get last successful artifacts that matches patterns""" |
| 203 | + build = self.get_last_successful_build() |
| 204 | + return build.list_artifacts_related_paths(artifact_path_patterns) |
| 205 | + |
| 206 | + def download_last_successful_artifacts( |
| 207 | + self, artifact_path_patterns=None, download_dir='.'): |
| 208 | + # type: (List[str])-> List[str] |
| 209 | + """Download last successful artifacts that matches patterns. |
| 210 | + Returns related path of artifacts |
| 211 | +
|
| 212 | + Note the directory structure will be flattern.""" |
| 213 | + if not artifact_path_patterns: |
| 214 | + artifact_path_patterns = ['.*'] |
| 215 | + build = self.get_last_successful_build() |
| 216 | + artifact_path_list = build.list_artifacts_related_paths( |
| 217 | + artifact_path_patterns) |
| 218 | + for artifact_path in artifact_path_list: |
| 219 | + UrlHelper.download_file( |
| 220 | + build.url + 'artifact/' + artifact_path, |
| 221 | + download_dir=download_dir) |
| 222 | + return artifact_path_list |
| 223 | + |
| 224 | + |
| 225 | +class JenkinsJobBuild(object): |
| 226 | + """Build object for Jenkins job""" |
| 227 | + |
| 228 | + def __init__(self, parent_job, build_number, build_url): |
| 229 | + # type (object, int, str) -> None |
| 230 | + self.parent_job = parent_job |
| 231 | + self.number = build_number |
| 232 | + self.url = build_url |
| 233 | + self.content = None |
| 234 | + |
| 235 | + def get_elem(self, path): |
| 236 | + # type: (str) -> object |
| 237 | + """Get element from the build object""" |
| 238 | + return JenkinsJob.dict_get_elem_by_path(self.content, path) |
| 239 | + |
| 240 | + def load(self): |
| 241 | + """Load the build object from Jenkins server""" |
| 242 | + logging.debug("Loading build from %sapi/python", self.url) |
| 243 | + self.content = ast.literal_eval(UrlHelper.read( |
| 244 | + "%s/api/python" % self.url)) |
| 245 | + |
| 246 | + def list_artifacts_related_paths(self, artifact_path_patterns=None): |
| 247 | + # type: (str) -> List[str] |
| 248 | + """Return a List of relativePaths of artifacts |
| 249 | + that matches the path patterns""" |
| 250 | + if not artifact_path_patterns: |
| 251 | + artifact_path_patterns = ['.*'] |
| 252 | + if not self.content: |
| 253 | + self.load() |
| 254 | + if not self.content: |
| 255 | + raise AssertionError("Failed to load build from %s" % self.url) |
| 256 | + result = [] |
| 257 | + for artifact in self.content['artifacts']: |
| 258 | + for pattern in artifact_path_patterns: |
| 259 | + if re.search(pattern, artifact['relativePath']): |
| 260 | + result.append(artifact['relativePath']) |
| 261 | + break # Only append once |
| 262 | + return result |
| 263 | + |
| 264 | + def __repr__(self): |
| 265 | + # type: () -> str |
| 266 | + result = "\n".join([ |
| 267 | + JenkinsJob.print_key_value( |
| 268 | + tup[0], str(tup[1])) for tup in [ |
| 269 | + ['number', self.number], |
| 270 | + ['url', self.url]]]) |
| 271 | + |
| 272 | + if self.content: |
| 273 | + result += "\n\n%s" % "\n".join([ |
| 274 | + JenkinsJob.print_key_value( |
| 275 | + key, self.get_elem(key)) for key in [ |
| 276 | + 'nextBuild/number', |
| 277 | + 'previousBuild/number']]) |
| 278 | + result += "\n\nArtifacts:\n%s" % "\n ".join( |
| 279 | + self.list_artifacts_related_paths()) |
| 280 | + return result |
| 281 | + |
| 282 | + |
| 283 | +def run_sub_command(args): |
| 284 | + # type (ZanataArgParser.Namespace) -> None |
| 285 | + """Run the sub command""" |
| 286 | + job = JenkinsJob.init_from_parsed_args(args) |
| 287 | + job.load() |
| 288 | + if args.sub_command == 'get_job': |
| 289 | + print(job) |
| 290 | + elif args.sub_command == 'get_last_successful_build': |
| 291 | + build = job.get_last_successful_build() |
| 292 | + build.load() |
| 293 | + print(build) |
| 294 | + elif args.sub_command == 'get_last_successful_artifacts': |
| 295 | + print('\n'.join( |
| 296 | + job.get_last_successful_artifacts( |
| 297 | + args.artifact_path_patterns.split(',')) |
| 298 | + )) |
| 299 | + elif args.sub_command == 'download_last_successful_artifacts': |
| 300 | + artifact_path_list = job.download_last_successful_artifacts( |
| 301 | + args.artifact_path_patterns.split(','), |
| 302 | + args.download_dir) |
| 303 | + print("Downloaded files %s" % '\n'.join(artifact_path_list)) |
| 304 | + |
| 305 | + |
| 306 | +if __name__ == '__main__': |
| 307 | + run_sub_command(JenkinsJob.add_parser().parse_all()) |
0 commit comments