Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2@file 

3@brief Extends Jenkins Server from :epkg:`python-jenkins`. 

4""" 

5 

6import os 

7import sys 

8import socket 

9import hashlib 

10import re 

11from xml.sax.saxutils import escape 

12import requests 

13import jenkins 

14from ..loghelper.flog import noLOG 

15from ..pycode.windows_scripts import windows_jenkins, windows_jenkins_any 

16from ..pycode.windows_scripts import windows_jenkins_27_conda, windows_jenkins_27_def 

17from ..pycode.linux_scripts import linux_jenkins, linux_jenkins_any 

18from ..pycode.build_helper import private_script_replacements 

19from .jenkins_exceptions import JenkinsExtException, JenkinsJobException 

20from .jenkins_server_template import _config_job, _trigger_up, _trigger_time, _git_repo, _task_batch_win, _task_batch_lin 

21from .jenkins_server_template import _trigger_startup, _publishers, _file_creation, _wipe_repo, _artifacts, _cleanup_repo 

22from .yaml_helper import enumerate_processed_yml 

23from .jenkins_helper import jenkins_final_postprocessing, get_platform 

24 

25_timeout_default = 1200 

26 

27_default_engine_paths = { 

28 "windows": { 

29 "__PY36__": "__PY36__", 

30 "__PY37__": "__PY37__", 

31 "__PY38__": "__PY38__", 

32 "__PY39__": "__PY39__", 

33 "__PY36_X64__": "__PY36_X64__", 

34 "__PY37_X64__": "__PY37_X64__", 

35 "__PY38_X64__": "__PY38_X64__", 

36 "__PY39_X64__": "__PY39_X64__", 

37 }, 

38} 

39 

40 

41def _modified_windows_jenkins(requirements_local, requirements_pypi, module="__MODULE__", 

42 port="__PORT__", platform=None): 

43 return private_script_replacements( 

44 linux_jenkins, module, 

45 (requirements_local, requirements_pypi), 

46 port, raise_exception=False, 

47 default_engine_paths=_default_engine_paths, 

48 platform=get_platform(platform)) 

49 

50 

51def _modified_linux_jenkins(requirements_local, requirements_pypi, module="__MODULE__", 

52 port="__PORT__", platform=None): 

53 return private_script_replacements( 

54 windows_jenkins, module, 

55 (requirements_local, requirements_pypi), 

56 port, raise_exception=False, 

57 default_engine_paths=_default_engine_paths, 

58 platform=get_platform(platform)) 

59 

60 

61def _modified_windows_jenkins_27(requirements_local, requirements_pypi, module="__MODULE__", 

62 port="__PORT__", anaconda=True, platform=None): 

63 return private_script_replacements( 

64 windows_jenkins_27_conda if anaconda else windows_jenkins_27_def, 

65 module, (requirements_local, requirements_pypi), 

66 port, raise_exception=False, 

67 default_engine_paths=_default_engine_paths, 

68 platform=get_platform(platform)) 

69 

70 

71def _modified_windows_jenkins_any(requirements_local, requirements_pypi, module="__MODULE__", 

72 port="__PORT__", platform=None): 

73 res = private_script_replacements( 

74 windows_jenkins_any, module, 

75 (requirements_local, requirements_pypi), 

76 port, raise_exception=False, 

77 default_engine_paths=_default_engine_paths, 

78 platform=get_platform(platform)) 

79 return res.replace("virtual_env_suffix=%2", "virtual_env_suffix=___SUFFIX__") 

80 

81 

82def _modified_linux_jenkins_any(requirements_local, requirements_pypi, module="__MODULE__", 

83 port="__PORT__", platform=None): 

84 res = private_script_replacements( 

85 linux_jenkins_any, module, 

86 (requirements_local, requirements_pypi), 

87 port, raise_exception=False, 

88 default_engine_paths=_default_engine_paths, 

89 platform=get_platform(platform)) 

90 return res.replace("virtual_env_suffix=%2", "virtual_env_suffix=___SUFFIX__") 

91 

92 

93class JenkinsExt(jenkins.Jenkins): 

94 

95 """ 

96 Extensions for the :epkg:`Jenkins` server 

97 based on module :epkg:`python-jenkins`. 

98 

99 .. index:: Jenkins, Jenkins extensions 

100 

101 Some useful :epkg:`Jenkins` extensions: 

102 

103 * `Credentials Plugin <https://wiki.jenkins-ci.org/display/JENKINS/Credentials+Plugin>`_ 

104 * `Extra Column Plugin <https://wiki.jenkins-ci.org/display/JENKINS/Extra+Columns+Plugin>`_ 

105 * `Git Client Plugin <https://wiki.jenkins-ci.org/display/JENKINS/Git+Client+Plugin>`_ 

106 * `GitHub Client Plugin <https://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin>`_ 

107 * `GitLab Client Plugin <https://wiki.jenkins-ci.org/display/JENKINS/GitLab+Plugin>`_ 

108 * `Matrix Project Plugin <https://wiki.jenkins-ci.org/display/JENKINS/Matrix+Project+Plugin>`_ 

109 * `Build Pipeline Plugin <https://wiki.jenkins-ci.org/display/JENKINS/Build+Pipeline+Plugin>`_ 

110 

111 The whole class can define many different engines. 

112 A job can send a mail at the end of the job execution. 

113 """ 

114 

115 _config_job = _config_job # pylint: disable=W0127 

116 _trigger_up = _trigger_up # pylint: disable=W0127 

117 _trigger_time = _trigger_time # pylint: disable=W0127 

118 _trigger_startup = _trigger_startup # pylint: disable=W0127 

119 _git_repo = _git_repo # pylint: disable=W0127 

120 _task_batch_win = _task_batch_win # pylint: disable=W0127 

121 _task_batch_lin = _task_batch_lin # pylint: disable=W0127 

122 _publishers = _publishers # pylint: disable=W0127 

123 _wipe_repo = _wipe_repo # pylint: disable=W0127 

124 _artifacts = _artifacts # pylint: disable=W0127 

125 _cleanup_repo = _cleanup_repo # pylint: disable=W0127 

126 

127 def __init__(self, url, username=None, password=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, 

128 mock=False, engines=None, platform=None, pypi_port=8067, fLOG=noLOG, 

129 mails=None): 

130 """ 

131 @param url url of the server 

132 @param username username 

133 @param password password 

134 @param timeout timeout 

135 @param mock True by default, if False, avoid talking to the server 

136 @param engines list of Python engines *{name: path to python.exe}* 

137 @param platform platform of the Jenkins server 

138 @param pypi_port pypi port used for the documentation server 

139 @param mails (str) list of mails to contact in case of a mistaje 

140 @param fLOG logging function 

141 

142 If *platform* is None, it is replace by the value returned 

143 by @see fn get_platform. 

144 """ 

145 if platform is None: 

146 platform = get_platform(platform) 

147 jenkins.Jenkins.__init__( 

148 self, url, username, password, timeout=timeout) 

149 self._mock = mock 

150 self.platform = platform 

151 self.pypi_port = pypi_port 

152 self.mails = mails 

153 self.fLOG = fLOG 

154 if engines is None: 

155 engines = {"default": os.path.dirname(sys.executable)} 

156 self.engines = engines 

157 for k, v in self.engines.items(): 

158 if v.endswith(".exe"): 

159 raise FileNotFoundError( # pragma: no cover 

160 "{}:{} is not a folder".format(k, v)) 

161 if " " in v: 

162 raise JenkinsJobException( # pragma: no cover 

163 "No space allowed in engine path: " + v) 

164 

165 @property 

166 def Engines(self): 

167 """ 

168 @return the available engines 

169 """ 

170 return self.engines 

171 

172 def jenkins_open(self, req, add_crumb=True, resolve_auth=True): # pragma: no cover 

173 ''' 

174 Overloads the same method from module :epkg:`python-jenkins` 

175 to replace string by bytes. 

176 

177 @param req see :epkg:`Jenkins API` 

178 @param add_crumb see :epkg:`Jenkins API` 

179 @param resolve_auth see :epkg:`Jenkins API` 

180 ''' 

181 if self._mock: 

182 raise JenkinsExtException("mocking server, cannot be open") 

183 

184 response = self.jenkins_request( 

185 req=req, add_crumb=add_crumb, resolve_auth=resolve_auth) 

186 if response is None: 

187 raise jenkins.EmptyResponseException( 

188 "Error communicating with server[%s]: " 

189 "empty response" % self.server) 

190 return response.content 

191 

192 def delete_job(self, name): # pragma: no cover 

193 ''' 

194 Deletes :epkg:`Jenkins` job permanently. 

195 

196 :param name: name of :epkg:`Jenkins` job, ``str`` 

197 ''' 

198 if self._mock: 

199 return 

200 r = self._get_job_folder(name) 

201 if r is None: 

202 raise JenkinsExtException('delete[%s] failed (no job)' % (name)) 

203 

204 folder_url, short_name = self._get_job_folder(name) 

205 if folder_url is None: 

206 raise ValueError("folder_url is None for job '{0}'".format(name)) 

207 self.jenkins_open(requests.Request( 

208 'POST', self._build_url(jenkins.DELETE_JOB, locals()) 

209 )) 

210 if self.job_exists(name) or self.job_exists(short_name): 

211 raise jenkins.JenkinsException('delete[%s] failed' % (name)) 

212 

213 def get_jobs(self, folder_depth=0, folder_depth_per_request=10, view_name=None): 

214 """ 

215 Gets the list of all jobs recursively to the given folder depth, 

216 see `get_all_jobs 

217 <https://python-jenkins.readthedocs.org/en/latest/api.html 

218 #jenkins.Jenkins.get_all_jobs>`_. 

219 

220 @return list of jobs, ``[ { str: str} ]`` 

221 """ 

222 return jenkins.Jenkins.get_jobs(self, folder_depth=folder_depth, 

223 folder_depth_per_request=folder_depth_per_request, 

224 view_name=view_name) 

225 

226 def delete_all_jobs(self): # pragma: no cover 

227 """ 

228 Deletes all jobs permanently. 

229 

230 @return list of deleted jobs 

231 """ 

232 jobs = self.get_jobs() 

233 res = [] 

234 for k in jobs: 

235 self.fLOG("[jenkins] remove job", k["name"]) 

236 self.delete_job(k["name"]) 

237 res.append(k["name"]) 

238 return res 

239 

240 def get_jenkins_job_name(self, job): 

241 """ 

242 Infers a name for the jenkins job. 

243 

244 @param job str 

245 @return name 

246 """ 

247 if "<--" in job: 

248 job = job.split("<--")[0] 

249 if job.startswith("custom "): 

250 return job.replace(" ", "_").replace("[", "").replace("]", "").strip("_") 

251 def_prefix = ["doc", "setup", "setup_big"] 

252 def_prefix.extend(self.engines.keys()) 

253 for prefix in def_prefix: 

254 p = "[%s]" % prefix 

255 if p in job: 

256 job = p + " " + job.replace(" " + p, "") 

257 return job.replace(" ", "_").replace("[", "").replace("]", "").strip("_") 

258 

259 def get_engine_from_job(self, job, return_key=False): 

260 """ 

261 Extracts the engine from the job definition, 

262 it should be like ``[engine]``. 

263 

264 @param job job string 

265 @param return_key return the engine name too 

266 @return engine or tuple(engine, name) 

267 

268 If their is no engine definition, the system 

269 uses the default one (key=*default*) if it was defined. 

270 Otherwise, it raises an exception. 

271 """ 

272 res = None 

273 spl = job.split() 

274 for s in spl: 

275 t = s.strip(" []") 

276 if t in self.engines: 

277 res = self.engines[t] 

278 key = t 

279 break 

280 if res is None and "default" in self.engines: 

281 res = self.engines["default"] 

282 key = "default" 

283 if res is None: 

284 raise JenkinsJobException( # pragma: no cover 

285 "Unable to find engine in job '{}', available: {}".format( 

286 job, ", ".join(self.engines.keys()))) 

287 if "[27]" in job and "python34" in res.lower(): # pragma: no cover 

288 mes = "\n".join(" {0}={1}".format(k, v) 

289 for k, v in sorted(self.engines.items())) 

290 raise ValueError( 

291 "Python mismatch in version:\nJOB = {0}\nRES = {1}\nENGINES\n{2}" 

292 "".format(job, res, mes)) 

293 return (res, key) if return_key else res 

294 

295 def get_cmd_standalone(self, job): 

296 """ 

297 Custom command for :epkg:`Jenkins` (such as updating conda) 

298 

299 @param job module and options 

300 @return script 

301 """ 

302 spl = job.split() 

303 if spl[0] != "standalone": 

304 raise JenkinsExtException( # pragma: no cover 

305 "the job should start by standalone: " + job) 

306 

307 if self.platform.startswith("win"): 

308 # windows 

309 if "[conda_update]" in spl: 

310 cmd = "__ENGINE__\\Scripts\\conda update -y --all" 

311 elif "[local_pypi]" in spl: 

312 cmd = "if not exist ..\\..\\local_pypi mkdir ..\\local_pypi" 

313 cmd += "\nif not exist ..\\..\\local_pypi\\local_pypi_server mkdir ..\\..\\local_pypi\\local_pypi_server" 

314 cmd += "\necho __ENGINE__\\..\\Scripts\\pypi-server.exe -v -u -p __PORT__ --disable-fallback " 

315 cmd += "..\\..\\local_pypi\\local_pypi_server > ..\\..\\local_pypi\\local_pypi_server\\start_local_pypi.bat" 

316 cmd = cmd.replace("__PORT__", str(self.pypi_port)) 

317 elif "[update]" in spl: 

318 cmd = "__ENGINE__\\python -u -c \"from pymyinstall.packaged import update_all;" 

319 cmd += "update_all(temp_folder='build/update_modules', " 

320 cmd += "verbose=True, source='2')\"" 

321 elif "[install]" in spl: 

322 cmd = "__ENGINE__\\python -u -c \"from pymyinstall.packaged import install_all;install_all" 

323 cmd += "(temp_folder='build/update_modules', " 

324 cmd += "verbose=True, source='2')\"" 

325 else: 

326 raise JenkinsExtException( 

327 "cannot interpret job: " + job) # pragma: no cover 

328 

329 engine = self.get_engine_from_job(job) 

330 cmd = cmd.replace("__ENGINE__", engine) 

331 return cmd 

332 else: # pragma: no cover 

333 if "[conda_update]" in spl: 

334 cmd = "__ENGINE__/bin/conda update -y --all" 

335 elif "[local_pypi]" in spl: 

336 cmd = 'if [-f ../local_pypi ]; then mkdir "../local_pypi"; fi' 

337 cmd += '\nif [-f ../local_pypi/local_pypi_server]; then mkdir "../local_pypi/local_pypi_server"; fi' 

338 cmd += "\necho pypi-server -v -u -p __PORT__ --disable-fallback " 

339 cmd += "../local_pypi/local_pypi_server > ../local_pypi/local_pypi_server/start_local_pypi.sh" 

340 cmd = cmd.replace("__PORT__", str(self.pypi_port)) 

341 elif "[update]" in spl: 

342 cmd = "__ENGINE__/python -u -c \"from pymyinstall.packaged import update_all;" 

343 cmd += "update_all(temp_folder='build/update_modules', " 

344 cmd += "verbose=True, source='2')\"" 

345 elif "[install]" in spl: 

346 cmd = "__ENGINE__/python -u -c \"from pymyinstall.packaged import install_all;install_all" 

347 cmd += "(temp_folder='build/update_modules', " 

348 cmd += "verbose=True, source='2')\"" 

349 else: 

350 raise JenkinsExtException("cannot interpret job: " + job) 

351 

352 engine = self.get_engine_from_job(job) 

353 cmd = cmd.replace("__ENGINE__", engine) 

354 return cmd 

355 

356 @staticmethod 

357 def get_cmd_custom(job): 

358 """ 

359 Custom script for :epkg:`Jenkins`. 

360 

361 @param job module and options 

362 @return script 

363 """ 

364 spl = job.split() 

365 if spl[0] != "custom": 

366 raise JenkinsExtException( 

367 "the job should start by custom: " + job) 

368 # we expect __SCRIPTOPTIONS__ to be replaced by a script later on 

369 return "__SCRIPTOPTIONS__" 

370 

371 @staticmethod 

372 def hash_string(s, le=4): 

373 """ 

374 Hashes a string. 

375 

376 @param s string 

377 @param le cut the string to the first *l* character 

378 @return hashed string 

379 """ 

380 m = hashlib.md5() 

381 m.update(s.encode("ascii")) 

382 r = m.hexdigest().upper() 

383 if len(r) < le: 

384 return r # pragma: no cover 

385 m = le // 2 

386 return r[:m] + r[len(r) - le + m:] 

387 

388 def extract_requirements(self, job): 

389 """ 

390 Extracts the requirements for a job. 

391 

392 @param job job name 

393 @return 3-tuple job, local requirements, pipy requirements 

394 

395 Example:: 

396 

397 "pyensae <-- pyquickhelper <---- qgrid" 

398 

399 The function returns:: 

400 

401 (pyensae, ["pyquickhelper"], ["qgrid"]) 

402 """ 

403 if "<--" in job: 

404 spl = job.split("<--") 

405 job = spl[0] 

406 rl, rp = None, None 

407 for o in spl[1:]: 

408 if o.startswith("--"): 

409 rp = [_.strip("- ") for _ in o.split(",")] 

410 else: 

411 rl = [_.strip() for _ in o.split(",")] 

412 return job, rl, rp 

413 return job, None, None 

414 

415 def get_jenkins_script(self, job): 

416 """ 

417 Builds the :epkg:`Jenkins` script for a module and its options. 

418 

419 @param job module and options 

420 @return script 

421 

422 Method @see me setup_jenkins_server describes which tags this method can interpret. 

423 The method allow command such as ``[custom...]``, they will be 

424 run in a virtual environment as ``setup.py custom...``. 

425 Parameter *job* can be ``empty``, in that case, this function returns an empty string. 

426 Requirements local and from pipy can be specified by added in the job name: 

427 

428 * ``<-- module1, module2`` for local requirements 

429 """ 

430 job_verbose = job 

431 

432 def replacements(cmd, engine, python, suffix, module_name): 

433 res = cmd.replace("__ENGINE__", engine) \ 

434 .replace("__PYTHON__", python) \ 

435 .replace("__SUFFIX__", suffix + "_" + job_hash) \ 

436 .replace("__PORT__", str(self.pypi_port)) \ 

437 .replace("__MODULE__", module_name) # suffix for the virtual environment and module name 

438 if "[27]" in job: 

439 res = res.replace("__PYTHON27__", python) 

440 if "__DEFAULTPYTHON__" in res: 

441 if "default" not in self.engines: 

442 raise JenkinsExtException( # pragma: no cover 

443 "a default engine (Python 3.4) must be defined for script using Python 27, job={}".format(job)) 

444 res = res.replace("__DEFAULTPYTHON__", 

445 os.path.join(self.engines["default"], "python")) 

446 

447 # patch for pyquickhelper 

448 if "PACTHPQ" in res: 

449 if hasattr(self, "PACTHPQ"): 

450 if not hasattr(self, "pyquickhelper"): 

451 raise RuntimeError( # pragma: no cover 

452 "this should not happen:\n{0}\n---\n{1}".format(job_verbose, res)) 

453 if "pyquickhelper" in module_name: 

454 repb = "@echo ~~SET set PYTHONPATH=src\nset PYTHONPATH=src" 

455 else: 

456 repb = "@echo ~~SET set PYTHONPATH={0}\nset PYTHONPATH={0}".format(self.pyquickhelper.replace( 

457 "\\\\", "\\")) 

458 repe = "@echo ~~SET set PYTHONPATH=\nset PYTHONPATH=" 

459 else: 

460 repb = "" 

461 repe = "" 

462 res = res.replace("__PACTHPQb__", repb).replace( 

463 "__PACTHPQe__", repe) 

464 

465 if "__" in res: 

466 raise JenkinsJobException( # pragma: no cover 

467 "unable to interpret command line: {}\nCMD: {}\nRES:\n{}".format(job_verbose, cmd, res)) 

468 

469 # patch to avoid installing pyquickhelper when testing 

470 # pyquickhelper 

471 if module_name == "pyquickhelper": 

472 lines = res.split("\n") 

473 for i, line in enumerate(lines): 

474 if "/simple/ pyquickhelper" in line and "--find-links http://localhost" in line: 

475 lines[i] = "" # pragma: no cover 

476 res = "\n".join(lines) 

477 

478 return res 

479 

480 # job hash 

481 job_hash = JenkinsExt.hash_string(job) 

482 

483 # extact requirements 

484 job, requirements_local, requirements_pypi = self.extract_requirements( 

485 job) 

486 spl = job.split() 

487 module_name = spl[0] 

488 

489 if self.platform.startswith("win"): 

490 # windows 

491 engine, namee = self.get_engine_from_job(job, True) 

492 python = os.path.join(engine, "python.exe") 

493 

494 if len(spl) == 1: 

495 script = _modified_windows_jenkins( 

496 requirements_local, requirements_pypi, platform=self.platform) 

497 if not isinstance(script, list): 

498 script = [script] 

499 return [replacements(s, engine, python, namee + "_" + job_hash, module_name) for s in script] 

500 

501 if len(spl) == 0: 

502 raise ValueError("job is empty") # pragma: no cover 

503 

504 if spl[0] == "standalone": 

505 # conda update 

506 return self.get_cmd_standalone(job) 

507 

508 if spl[0] == "custom": 

509 # custom script 

510 return JenkinsExt.get_cmd_custom(job) 

511 

512 if spl[0] == "empty": 

513 return "" # pragma: no cover 

514 

515 if len(spl) in [2, 3, 4, 5]: 

516 # step 1: define the script 

517 

518 if "[test_local_pypi]" in spl: # pragma: no cover 

519 cmd = """__PYTHON__ -u setup.py test_local_pypi""" 

520 cmd = "auto_setup_test_local_pypi.bat __PYTHON__" 

521 elif "[update_modules]" in spl: 

522 cmd = """__PYTHON__ -u -c "import sys;sys.path.append('src');from pymyinstall.packaged import update_all;""" + \ 

523 """update_all(temp_folder='build/update_modules', verbose=True, source='2')" """ 

524 elif "[UT]" in spl: 

525 parameters = [_ for _ in spl if _.startswith( 

526 "{") and _.endswith("}")] 

527 if len(parameters) != 1: 

528 raise ValueError( # pragma: no cover 

529 "Unable to extract parameters for the unittests:" 

530 "\n{0}".format(" ".join(spl))) 

531 p = parameters[0].replace("_", " ").strip("{}") 

532 cmd = _modified_windows_jenkins_any(requirements_local, requirements_pypi, platform=self.platform).replace( 

533 "__COMMAND__", "unittests " + p) 

534 elif "[LONG]" in spl: 

535 cmd = _modified_windows_jenkins_any(requirements_local, requirements_pypi, platform=self.platform).replace( 

536 "__COMMAND__", "unittests_LONG") 

537 elif "[SKIP]" in spl: 

538 cmd = _modified_windows_jenkins_any( # pragma: no cover 

539 requirements_local, requirements_pypi, platform=self.platform).replace( 

540 "__COMMAND__", "unittests_SKIP") 

541 elif "[GUI]" in spl: 

542 cmd = _modified_windows_jenkins_any( # pragma: no cover 

543 requirements_local, requirements_pypi, platform=self.platform).replace( 

544 "__COMMAND__", "unittests_GUI") 

545 elif "[27]" in spl: 

546 cmd = _modified_windows_jenkins_27( 

547 requirements_local, requirements_pypi, anaconda=" [anaconda" in job, platform=self.platform) 

548 if not isinstance(cmd, list): 

549 cmd = [cmd] # pragma: no cover 

550 else: 

551 cmd = list(cmd) 

552 if spl[0] == "pyquickhelper": 

553 # exception for this job, we don't want to import pyquickhelper 

554 # c:/jenkins/pymy/anaconda2_pyquickhelper_27/../virtual/pyquickhelper_conda27vir/Scripts/pip 

555 # install --no-cache-dir --index 

556 # http://localhost:8067/simple/ pyquickhelper 

557 for i in range(0, len(cmd)): 

558 lines = cmd[i].split("\n") 

559 lines = [ 

560 (_ if "simple/ pyquickhelper" not in _ else "rem do not import pyquickhelper") for _ in lines] 

561 cmd[i] = "\n".join(lines) 

562 elif "[doc]" in spl: 

563 # documentation 

564 cmd = _modified_windows_jenkins_any(requirements_local, requirements_pypi, platform=self.platform).replace( 

565 "__COMMAND__", "build_sphinx") 

566 else: 

567 cmd = _modified_windows_jenkins( 

568 requirements_local, requirements_pypi, platform=self.platform) 

569 for pl in spl[1:]: 

570 if pl.startswith("[custom_") and pl.endswith("]"): 

571 cus = pl.strip("[]") 

572 cmd = _modified_windows_jenkins_any(requirements_local, requirements_pypi, 

573 platform=self.platform).replace("__COMMAND__", cus) 

574 

575 # step 2: replacement (python __PYTHON__, virtual environnement 

576 # __SUFFIX__) 

577 

578 cmds = cmd if isinstance(cmd, list) else [cmd] 

579 res = [] 

580 for cmd in cmds: 

581 cmdn = replacements(cmd, engine, python, 

582 namee + "_" + job_hash, module_name) 

583 if "run27" in cmdn and ( 

584 "Python34" in cmdn or "Python35" in cmdn or 

585 "Python36" in cmdn or "Python37" in cmdn or 

586 "Python38" in cmdn or "Python39" in cmdn): 

587 raise ValueError( # pragma: no cover 

588 "Python version mismatch\nENGINE\n{2}\n----BEFORE" 

589 "\n{0}\n-----\nAFTER\n-----\n{1}".format(cmd, cmdn, engine)) 

590 res.append(cmdn) 

591 

592 return res 

593 else: 

594 raise ValueError("unable to interpret: " + 

595 job) # pragma: no cover 

596 else: # pragma: no cover 

597 # linux 

598 engine, namee = self.get_engine_from_job(job, True) 

599 if engine is None: 

600 python = "python%d.%d" % sys.version_info[:2] 

601 elif namee.startswith('py'): 

602 vers = (int(namee[2:3]), int(namee[3:])) 

603 python = "python%d.%d" % vers 

604 else: 

605 raise ValueError( 

606 "Unable to handle engine ='{}', namee='{}'.".format(engine, namee)) 

607 

608 if len(spl) == 1: 

609 script = _modified_linux_jenkins( 

610 requirements_local, requirements_pypi, platform=self.platform) 

611 if not isinstance(script, list): 

612 script = [script] 

613 return [replacements(s, engine, python, namee + "_" + job_hash, module_name) for s in script] 

614 

615 elif len(spl) == 0: 

616 raise ValueError("job is empty") 

617 

618 elif spl[0] == "standalone": 

619 # conda update 

620 return self.get_cmd_standalone(job) 

621 

622 elif spl[0] == "empty": 

623 return "" 

624 

625 elif len(spl) in [2, 3, 4, 5]: 

626 # step 1: define the script 

627 

628 if "[test_local_pypi]" in spl: 

629 cmd = """__PYTHON__ -u setup.py test_local_pypi""" 

630 cmd = "auto_setup_test_local_pypi.bat __PYTHON__" 

631 elif "[update_modules]" in spl: 

632 cmd = """__PYTHON__ -u -c "import sys;sys.path.append('src');from pymyinstall.packaged import update_all;""" + \ 

633 """update_all(temp_folder='build/update_modules', verbose=True, source='2')" """ 

634 

635 else: 

636 cmd = _modified_linux_jenkins( 

637 requirements_local, requirements_pypi, platform=self.platform) 

638 for pl in spl[1:]: 

639 if pl.startswith("[custom_") and pl.endswith("]"): 

640 cus = pl.strip("[]") 

641 cmd = _modified_linux_jenkins_any(requirements_local, requirements_pypi, 

642 platform=self.platform).replace("__COMMAND__", cus) 

643 

644 # step 2: replacement (python __PYTHON__, virtual environnement 

645 # __SUFFIX__) 

646 

647 cmds = cmd if isinstance(cmd, list) else [cmd] 

648 res = [] 

649 for cmd in cmds: 

650 cmdn = replacements(cmd, engine, python, 

651 namee + "_" + job_hash, module_name) 

652 if "run27" in cmdn and ( 

653 "Python34" in cmdn or "Python35" in cmdn or 

654 "Python36" in cmdn or "Python37" in cmdn or 

655 "Python38" in cmdn or "Python39" in cmdn): 

656 raise ValueError( 

657 "Python version mismatch\nENGINE\n{2}\n----BEFORE\n{0}\n-----\nAFTER\n-----\n{1}".format(cmd, cmdn, engine)) 

658 res.append(cmdn) 

659 

660 return res 

661 

662 # other possibilities 

663 raise NotImplementedError("On Linux, unable to interpret: " + job) 

664 

665 def adjust_scheduler(self, scheduler, adjust_scheduler=True): 

666 """ 

667 Adjusts the scheduler to avoid having two jobs starting at the same time, 

668 jobs are delayed by an hour, two hours, three hours... 

669 

670 @param scheduler existing scheduler 

671 @param adjust_scheduler True to change it 

672 @return new scheduler (only hours are changed) 

673 

674 The function uses member ``_scheduled_jobs``. 

675 It creates it if it does not exist. 

676 """ 

677 if not adjust_scheduler: 

678 return scheduler # pragma: no cover 

679 if scheduler is None: 

680 raise ValueError("scheduler is None") # pragma: no cover 

681 if not hasattr(self, "_scheduled_jobs"): 

682 self._scheduled_jobs = {} 

683 if scheduler not in self._scheduled_jobs: 

684 self._scheduled_jobs[scheduler] = 1 

685 return scheduler 

686 else: 

687 if "H(" in scheduler: 

688 cp = re.compile("H[(]([0-9]+-[0-9]+)[)]") 

689 f = cp.findall(scheduler) 

690 if len(f) != 1: 

691 raise ValueError( # pragma: no cover 

692 "Unable to find hours in the scheduler '{0}', expects 'H(a-b)'".format(scheduler)) 

693 a, b = f[0].split('-') 

694 a0 = a 

695 a = int(a) 

696 b = int(b) 

697 new_value = scheduler 

698 rep = 'H(%s)' % f[0] 

699 iter = 0 

700 while iter < 100 and (new_value in self._scheduled_jobs or (a0 == a)): 

701 a += 1 

702 b += 1 

703 if a >= 24 or b > 24: 

704 a = 0 # pragma: no cover 

705 b = 1 + iter // 24 # pragma: no cover 

706 r = 'H(%d-%d)' % (a, b) 

707 new_value = scheduler.replace(rep, r) 

708 iter += 1 

709 scheduler = new_value 

710 self._scheduled_jobs[ 

711 scheduler] = self._scheduled_jobs.get(scheduler, 0) + 1 

712 return scheduler 

713 

714 def create_job_template(self, name, git_repo, credentials="", upstreams=None, script=None, 

715 location=None, keep=10, scheduler=None, py27=False, description=None, 

716 default_engine_paths=None, success_only=False, update=False, 

717 timeout=_timeout_default, additional_requirements=None, 

718 return_job=False, adjust_scheduler=True, clean_repo=True, **kwargs): 

719 """ 

720 Adds a job to the :epkg:`Jenkins` server. 

721 

722 @param name name 

723 @param credentials credentials 

724 @param git_repo git repository 

725 @param upstreams the build must run after... (even if failures), 

726 must be None in that case 

727 @param script script to execute or list of scripts 

728 @param keep number of builds to keep 

729 @param location location of the build 

730 @param scheduler add a schedule time (upstreams must be None in that case) 

731 @param py27 python 2.7 (True) or Python 3 (False) 

732 @param description add a description to the job 

733 @param default_engine_paths define the default location for python engine, 

734 should be dictionary ``{ engine: path }``, see below. 

735 @param success_only only triggers the job if the previous one was successful 

736 @param update update the job instead of creating it 

737 @param additional_requirements requirements for this module built by this Jenkins server, 

738 otherthise, we assume they are available 

739 on the installed distribution 

740 @param timeout specify a timeout 

741 @param kwargs additional parameters 

742 @param adjust_scheduler adjust the scheduler of a job so that it is delayed if this spot 

743 is already taken 

744 @param return_job return job instead of submitting the job 

745 @param clean_repo clean the repository before building (default is yes) 

746 

747 The job can be modified on Jenkins. To add a time trigger:: 

748 

749 H H(13-14) * * * 

750 

751 Same trigger but once every week and not every day (Sunday for example):: 

752 

753 H H(13-14) * * 0 

754 

755 Parameter *success_only* prevents a job from running if the previous one failed. 

756 Options *success_only* must be specified. 

757 Parameter *update* updates a job instead of creating it. 

758 """ 

759 if 'platform' in kwargs: 

760 raise NameError( # pragma: no cover 

761 "Parameter 'platform' should be set up in the constructor.") 

762 if script is None: 

763 if self.platform.startswith("win"): 

764 if default_engine_paths is None and "default" in self.engines: 

765 ver = "__PY%d%d__" % sys.version_info[:2] 

766 pat = os.path.join(self.engines["default"], "python") 

767 default_engine_paths = dict( 

768 windows={ver: pat, "__PYTHON__": pat}) 

769 

770 script = private_script_replacements( 

771 windows_jenkins, "____", additional_requirements, "____", 

772 raise_exception=False, platform=self.platform, 

773 default_engine_paths=default_engine_paths) 

774 

775 hash = JenkinsExt.hash_string(script) 

776 script = script.replace("__SUFFIX__", hash) 

777 else: 

778 raise JenkinsExtException( 

779 "no default script for linux") # pragma: no cover 

780 

781 if upstreams is not None and len(upstreams) > 0 and scheduler is not None: 

782 raise JenkinsExtException( 

783 "upstreams and scheduler cannot be not null at the same time: {0}".format(name)) 

784 

785 # overwrite parameters with job_options 

786 job_options = kwargs.get('job_options', None) 

787 if job_options is not None: 

788 job_options = job_options.copy() 

789 if "scheduler" in job_options: 

790 scheduler = job_options["scheduler"] 

791 del job_options["scheduler"] 

792 if "git_repo" in job_options: 

793 git_repo = job_options["git_repo"] 

794 del job_options["git_repo"] 

795 if "credentials" in job_options: 

796 credentials = job_options["credentials"] 

797 del job_options["credentials"] 

798 

799 if upstreams is not None and len(upstreams) > 0: 

800 trigger = JenkinsExt._trigger_up \ 

801 .replace("__UP__", ",".join(upstreams)) \ 

802 .replace("__FAILURE__", "SUCCESS" if success_only else "FAILURE") \ 

803 .replace("__ORDINAL__", "0" if success_only else "2") \ 

804 .replace("__COLOR__", "BLUE" if success_only else "RED") 

805 elif scheduler is not None: 

806 if scheduler.lower() == "startup": 

807 trigger = JenkinsExt._trigger_startup 

808 elif scheduler.lower() == "NONE": 

809 trigger = "" # pragma: no cover 

810 else: 

811 new_scheduler = self.adjust_scheduler( 

812 scheduler, adjust_scheduler) 

813 trigger = JenkinsExt._trigger_time.replace( 

814 "__SCHEDULER__", new_scheduler) 

815 if description is not None: 

816 description = description.replace(scheduler, new_scheduler) 

817 scheduler = new_scheduler 

818 else: 

819 trigger = "" 

820 

821 if not isinstance(script, list): 

822 script = [script] 

823 

824 underscore = re.compile("(__[A-Z_]+__)") 

825 

826 # we modify the scripts 

827 script_mod = [] 

828 for scr in script: 

829 search = underscore.search(scr) 

830 if search: 

831 raise ValueError( # pragma: no cover 

832 "script still contains __\ndefault_engine_paths: {}\n" 

833 "found: {}\nscr:\n{}\nSCRIPT:\n{}\n".format( 

834 default_engine_paths, search.groups()[0], 

835 scr, str(script))) 

836 script_mod.append(scr) 

837 

838 # wrappers 

839 bwrappers = [] 

840 

841 # repo 

842 if clean_repo: 

843 wipe = JenkinsExt._wipe_repo 

844 bwrappers.append(JenkinsExt._cleanup_repo) 

845 else: 

846 wipe = "" 

847 if git_repo is None: 

848 git_repo_xml = "" 

849 else: 

850 if not isinstance(git_repo, str): 

851 raise TypeError( # pragma: no cover 

852 "git_repo must be str not '{0}'".format(git_repo)) 

853 git_repo_xml = JenkinsExt._git_repo \ 

854 .replace("__GITREPO__", git_repo) \ 

855 .replace("__WIPE__", wipe) \ 

856 .replace("__CRED__", "<credentialsId>%s</credentialsId>" % credentials) 

857 

858 # additional scripts 

859 before = [] 

860 if job_options is not None: 

861 if 'scripts' in job_options: 

862 lscripts = job_options['scripts'] 

863 for scr in lscripts: 

864 au = _file_creation.replace("__FILENAME__", scr["name"]) \ 

865 .replace("__CONTENT__", scr["content"]) 

866 if "__" in au: 

867 raise Exception( 

868 "Unable to fully replace expected string in:\n{0}".format(au)) 

869 before.append(au) 

870 del job_options['scripts'] 

871 if len(job_options) > 0: 

872 keys = ", ".join( 

873 ["credentials", "git_repo", "scheduler", "scripts"]) 

874 raise ValueError( # pragma: no cover 

875 "Unable to process options\n{0}\nYou can specify the " 

876 "following options:\n{1}".format(job_options, keys)) 

877 

878 # scripts 

879 # tasks is XML, we need to encode s into XML format 

880 if self.platform.startswith("win"): 

881 scr = JenkinsExt._task_batch_win 

882 else: 

883 scr = JenkinsExt._task_batch_lin 

884 tasks = before + [scr.replace("__SCRIPT__", escape(s)) 

885 for s in script_mod] 

886 

887 # location 

888 if location is not None and "<--" in location: 

889 raise Exception( # pragma: no cover 

890 "this should not happen") 

891 location = "" if location is None else "<customWorkspace>%s</customWorkspace>" % location 

892 

893 # emailing 

894 publishers = [] 

895 mails = kwargs.get("mails", None) 

896 if mails is not None: 

897 publishers.append( # pragma: no cover 

898 JenkinsExt._publishers.replace("__MAIL__", mails)) 

899 publishers.append(JenkinsExt._artifacts.replace( 

900 "__PATTERN__", "dist/*.whl,dist/*.zip")) 

901 

902 # replacements 

903 conf = JenkinsExt._config_job 

904 rep = dict(__KEEP__=str(keep), 

905 __TASKS__="\n".join(tasks), 

906 __TRIGGER__=trigger, 

907 __LOCATION__=location, 

908 __DESCRIPTION__="" if description is None else description, 

909 __GITREPOXML__=git_repo_xml, 

910 __TIMEOUT__=str(timeout), 

911 __PUBLISHERS__="\n".join(publishers), 

912 __BUILDWRAPPERS__="\n".join(bwrappers)) 

913 

914 for k, v in rep.items(): 

915 conf = conf.replace(k, v) 

916 

917 # final processing 

918 conf = jenkins_final_postprocessing(conf, py27) 

919 

920 if self._mock or return_job: 

921 return conf 

922 if update: 

923 return self.reconfig_job(name, conf) # pragma: no cover 

924 return self.create_job(name, conf) # pragma: no cover 

925 

926 def process_options(self, script, options): 

927 """ 

928 Postprocesses a script inserted in a job definition. 

929 

930 @param script script to execute (in a list) 

931 @param options dictionary with options 

932 @return new script 

933 """ 

934 if not isinstance(script, list): 

935 script = [script] 

936 for k, v in options.items(): 

937 if k == "pre": 

938 script.insert(0, v) 

939 elif k == "post": 

940 script.append(v) 

941 elif k == "pre_set": 

942 script = [v + "\n" + _ for _ in script] # pragma: no cover 

943 elif k == "post_set": 

944 script = [_ + "\n" + v for _ in script] # pragma: no cover 

945 elif k == "script": 

946 script = [_.replace("__SCRIPTOPTIONS__", v) for _ in script] 

947 else: 

948 raise JenkinsJobException( # pragma: no cover 

949 "unable to interpret options: " + str(options)) 

950 return script 

951 

952 def setup_jenkins_server(self, github, modules, get_jenkins_script=None, overwrite=False, 

953 location=None, prefix="", credentials="", update=True, yml_engine="jinja2", 

954 add_environ=True, disable_schedule=False, adjust_scheduler=True): 

955 """ 

956 Sets up many jobs in :epkg:`Jenkins`. 

957 

958 @param github github account if it does not start with *http://*, 

959 the link to git repository of the project otherwise, 

960 we assume all jobs in *modules* are located on the same 

961 account otherwise the function will have to called twice with 

962 different parameters 

963 @param modules modules for which to generate the 

964 @param get_jenkins_script see @see me get_jenkins_script (default value if this parameter is None) 

965 @param overwrite do not create the job if it already exists 

966 @param location None for default or a local folder 

967 @param prefix add a prefix to the name 

968 @param credentials credentials to use for the job (string or dictionary) 

969 @param update update job instead of deleting it if the job already exists 

970 @param yml_engine templating engine used to process yaml config files 

971 @param add_environ use of local environment variables to interpret the job 

972 @param adjust_scheduler adjust the scheduler of a job so that it is delayed if this spot is already taken 

973 @param disable_schedule disable scheduling for all jobs 

974 @return list of created jobs 

975 

976 If *credentials* are a dictionary, the function looks up 

977 into it by using the git repository as a key. If it does not find 

978 it, it looks for default key. If there is not found, 

979 the function assumes, there is not credentials for this git repository. 

980 

981 The function *get_jenkins_script* is called with the following parameters: 

982 

983 * job 

984 

985 The extension 

986 `Extra Columns Plugin <https://wiki.jenkins-ci.org/display/JENKINS/Extra+Columns+Plugin>`_ 

987 is very useful to add extra columns to a view (the description, the output of the 

988 last execution). Here is a list of useful extensions: 

989 

990 * `Build Graph View Plugin <https://wiki.jenkins-ci.org/display/JENKINS/Build+Graph+View+Plugin>`_ 

991 * `Build Pipeline Plugin <https://wiki.jenkins-ci.org/display/JENKINS/Build+Pipeline+Plugin>`_ 

992 * `Credentials Plugin <https://wiki.jenkins-ci.org/display/JENKINS/Credentials+Plugin>`_ 

993 * `Extra Columns Plugin <https://wiki.jenkins-ci.org/display/JENKINS/Extra+Columns+Plugin>`_ 

994 * `Git Plugin <https://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin>`_ 

995 * `GitHub Plugin <https://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin>`_ 

996 * `GitLab Plugin <https://wiki.jenkins-ci.org/display/JENKINS/GitLab+Plugin>`_ 

997 * :epkg:`Python` 

998 * `Python Wrapper Plugin <https://wiki.jenkins-ci.org/display/JENKINS/Python+Wrapper+Plugin>`_ 

999 * `Build timeout plugin <https://wiki.jenkins-ci.org/display/JENKINS/Build-timeout+Plugin>`_ 

1000 

1001 Tag description: 

1002 

1003 * ``[engine]``: to use this specific engine (Python path) 

1004 * ``[27]``: run with python 2.7 

1005 * ``[LONG]``: run longer unit tests (files start by ``test_LONG_``) 

1006 * ``[SKIP]``: run skipped unit tests (files start by ``test_SKIP_``) 

1007 * ``[GUI]``: run skipped unit tests (files start by ``test_GUI_``) 

1008 * ``[custom.+]``: run ``setup.py <custom.+>`` in a virtual environment 

1009 * ``[UT] {-d_10}``: run ``setup.py unittests -d 10`` in a virtual environment, ``-d 10`` is one of the possible parameters 

1010 

1011 Others tags: 

1012 

1013 * ``[conda_update]``: update conda distribution 

1014 * ``[update]``: update distribution 

1015 * ``[install]``: update distribution 

1016 * ``[local_pypi]``: write a script to run a local pypi server on port 8067 (default option) 

1017 * ``pymyinstall [update_modules]``: run a script to update all modules 

1018 (might have to be ran a couple of times before being successful) 

1019 

1020 *modules* is a list defined as follows: 

1021 

1022 * each element can be a string or a tuple (string, schedule time) or a list 

1023 * if it is a list, it contains a list of elements defined as previously 

1024 * if the job at position i is not scheduled, it will start after the last 

1025 job at position i-1 whether or not it fails 

1026 * the job can be defined as a tuple of 3 elements, the last one contains options 

1027 

1028 The available options are: 

1029 

1030 * pre: defines a string to insert at the beginning of a job 

1031 * post: defines a string to insert at the end of a job 

1032 * script: defines a full script if the job to execute is ``custom`` 

1033 

1034 Example :: 

1035 

1036 modules=[ # update anaconda 

1037 ("standalone [conda_update] [anaconda3]", 

1038 "H H(0-1) * * 0"), 

1039 "standalone [conda_update] [anaconda2] [27]", 

1040 "standalone [local_pypi]", 

1041 #"standalone [install]", 

1042 #"standalone [update]", 

1043 #"standalone [install] [py34]", 

1044 #"standalone [update] [py34]", 

1045 #"standalone [install] [winpython]", 

1046 #"standalone [update] [winpython]", 

1047 # pyquickhelper and others, 

1048 ("pyquickhelper", "H H(2-3) * * 0"), 

1049 ("pysqllike <-- pyquickhelper", None, dict(success_only=True)), 

1050 ["python3_module_template <-- pyquickhelper", 

1051 "pyquickhelper [27] [anaconda2]"], 

1052 ["pyquickhelper [winpython]", 

1053 "python3_module_template [27] [anaconda2] <-- pyquickhelper", ], 

1054 ["pymyinstall <-- pyquickhelper", "pyensae <-- pyquickhelper"], 

1055 ["pymmails <-- pyquickhelper", "pyrsslocal <-- pyquickhelper, pyensae"], 

1056 ["pymyinstall [27] [anaconda2] <-- pyquickhelper", "pymyinstall [LONG] <-- pyquickhelper"], 

1057 # update, do not move, it depends on pyquickhelper 

1058 ("pyquickhelper [anaconda3]", "H H(2-3) * * 1"), 

1059 ["pyquickhelper [winpython]", "pysqllike [anaconda3]", 

1060 "pysqllike [winpython] <-- pyquickhelper", 

1061 "python3_module_template [anaconda3] <-- pyquickhelper", 

1062 "python3_module_template [winpython] <-- pyquickhelper", 

1063 "pymmails [anaconda3] <-- pyquickhelper", 

1064 "pymmails [winpython] <-- pyquickhelper", 

1065 "pymyinstall [anaconda3] <-- pyquickhelper", 

1066 "pymyinstall [winpython] <-- pyquickhelper"], 

1067 ["pyensae [anaconda3] <-- pyquickhelper", 

1068 "pyensae [winpython] <-- pyquickhelper", 

1069 "pyrsslocal [anaconda3] <-- pyquickhelper, pyensae", 

1070 "pyrsslocal [winpython] <-- pyquickhelper"], 

1071 ("pymyinstall [update_modules]", 

1072 "H H(0-1) * * 5"), 

1073 "pymyinstall [update_modules] [winpython]", 

1074 "pymyinstall [update_modules] [py34]", 

1075 "pymyinstall [update_modules] [anaconda2]", 

1076 "pymyinstall [update_modules] [anaconda3]", 

1077 # py35 

1078 ("pyquickhelper [py34]", "H H(2-3) * * 2"), 

1079 ["pysqllike [py34]", 

1080 "pymmails [py34] <-- pyquickhelper", 

1081 "python3_module_template [py34] <-- pyquickhelper", 

1082 "pymyinstall [py34] <-- pyquickhelper"], 

1083 "pyensae [py34] <-- pyquickhelper", 

1084 "pyrsslocal [py34] <-- pyquickhelper, pyensae", 

1085 ], 

1086 

1087 Example:: 

1088 

1089 from ensae_teaching_cs.automation.jenkins_helper import setup_jenkins_server 

1090 from pyquickhelper.jenkinshelper import JenkinsExt 

1091 

1092 engines = dict(Anaconda2=r"C:\\Anaconda2", 

1093 Anaconda3=r"C:\\Anaconda3", 

1094 py35=r"c:\\Python35_x64", 

1095 py36=r"c:\\Python36_x64", 

1096 default=r"c:\\Python36_x64", 

1097 custom=r"c:\\CustomPython") 

1098 

1099 js = JenkinsExt('http://machine:8080/', "user", "password", engines=engines) 

1100 

1101 if True: 

1102 js.setup_jenkins_server(github="sdpython", overwrite = True, 

1103 location = r"c:\\jenkins\\pymy") 

1104 

1105 

1106 Another example:: 

1107 

1108 import sys 

1109 sys.path.append(r"C:\\<path>\\ensae_teaching_cs\\src") 

1110 sys.path.append(r"C:\\<path>\\pyquickhelper\\src") 

1111 sys.path.append(r"C:\\<path>\\pyensae\\src") 

1112 sys.path.append(r"C:\\<path>\\pyrsslocal\\src") 

1113 from ensae_teaching_cs.automation.jenkins_helper import setup_jenkins_server, JenkinsExt 

1114 js = JenkinsExt("http://<machine>:8080/", <user>, <password>) 

1115 js.setup_jenkins_server(location=r"c:\\jenkins\\pymy", overwrite=True, engines=engines) 

1116 

1117 Parameter *credentials* can be a dictionary where the key is 

1118 the git repository. Parameter *dependencies* and *no_dep* 

1119 were removed. Dependencies are now specified 

1120 in the job name using ``<--`` and they exclusively rely 

1121 on pipy (local or remote). Add options for module 

1122 *Build Timeout Plugin*. 

1123 """ 

1124 # we do a patch for pyquickhelper 

1125 all_jobs = [] 

1126 for jobs in modules: 

1127 jobs = jobs if isinstance(jobs, list) else [jobs] 

1128 for job in jobs: 

1129 if isinstance(job, tuple): 

1130 job = job[0] 

1131 job = job.split("<--")[0] 

1132 name = self.get_jenkins_job_name(job) 

1133 all_jobs.append(name) 

1134 all_jobs = set(all_jobs) 

1135 if "pyquickhelper" in all_jobs: 

1136 self.PACTHPQ = True 

1137 self.pyquickhelper = os.path.join( 

1138 location, "_pyquickhelper", "src") 

1139 

1140 # rest of the function 

1141 if get_jenkins_script is None: 

1142 get_jenkins_script = JenkinsExt.get_jenkins_script 

1143 

1144 if github is not None and "https://" not in github: 

1145 github = "https://github.com/" + github + "/" 

1146 

1147 deps = [] 

1148 created = [] 

1149 locations = [] 

1150 indexes = dict(order=0, dozen="A") 

1151 counts = {} 

1152 for jobs in modules: 

1153 if isinstance(jobs, tuple): 

1154 if len(jobs) == 0: 

1155 raise ValueError( 

1156 "Empty jobs in the list.") # pragma: no cover 

1157 if jobs[0] == "yml" and len(jobs) != 3: 

1158 raise ValueError( # pragma: no cover 

1159 "If it is a yml jobs, the tuple should contain 3 elements: ('yml', filename, schedule or None or dictionary).\n" + 

1160 "Not: {0}".format(jobs)) 

1161 

1162 cre, ds, locs = self._setup_jenkins_server_modules_loop( 

1163 jobs=jobs, counts=counts, 

1164 get_jenkins_script=get_jenkins_script, 

1165 location=location, adjust_scheduler=adjust_scheduler, 

1166 add_environ=add_environ, yml_engine=yml_engine, 

1167 overwrite=overwrite, prefix=prefix, 

1168 credentials=credentials, github=github, 

1169 disable_schedule=disable_schedule, jenkins_server=self, 

1170 update=update, indexes=indexes, deps=deps) 

1171 created.extend(cre) 

1172 locations.extend(locs) 

1173 deps.extend(ds) 

1174 return created 

1175 

1176 def _setup_jenkins_server_modules_loop(self, jobs, counts, get_jenkins_script, location, adjust_scheduler, 

1177 add_environ, yml_engine, overwrite, prefix, credentials, github, 

1178 disable_schedule, jenkins_server, update, indexes, deps): 

1179 if not isinstance(jobs, list): 

1180 jobs = [jobs] 

1181 indexes["unit"] = 0 

1182 new_dep = [] 

1183 created = [] 

1184 locations = [] 

1185 for i, job in enumerate(jobs): 

1186 indexes["unit"] += 1 

1187 cre, dep, loc = self._setup_jenkins_server_job_iteration( 

1188 job, counts=counts, 

1189 get_jenkins_script=get_jenkins_script, 

1190 location=location, adjust_scheduler=adjust_scheduler, 

1191 add_environ=add_environ, yml_engine=yml_engine, 

1192 overwrite=overwrite, prefix=prefix, 

1193 credentials=credentials, github=github, 

1194 disable_schedule=disable_schedule, 

1195 jenkins_server=jenkins_server, 

1196 update=update, indexes=indexes, 

1197 deps=deps, i=i) 

1198 created.extend(cre) 

1199 new_dep.extend(dep) 

1200 locations.extend(loc) 

1201 if len(new_dep) > 20000: 

1202 raise JenkinsExtException( # pragma: no cover 

1203 "unreasonable number of dependencies: {0}".format(len(new_dep))) 

1204 return created, new_dep, locations 

1205 

1206 def _setup_jenkins_server_job_iteration(self, job, get_jenkins_script, location, adjust_scheduler, 

1207 add_environ, yml_engine, overwrite, prefix, credentials, github, 

1208 disable_schedule, jenkins_server, update, indexes, deps, i, counts): 

1209 order = indexes["order"] 

1210 dozen = indexes["dozen"] 

1211 unit = indexes["unit"] 

1212 new_dep = [] 

1213 created = [] 

1214 locations = [] 

1215 

1216 if isinstance(job, tuple): 

1217 if len(job) < 2: 

1218 raise JenkinsJobException( # pragma: no cover 

1219 "the tuple must contain at least two elements:\nJOB:" 

1220 "\n" + str(job)) 

1221 

1222 if job[0] == "yml": 

1223 is_yml = True 

1224 job = job[1:] 

1225 else: 

1226 is_yml = False 

1227 

1228 # we extract options if any 

1229 if len(job) == 3: 

1230 options = job[2] 

1231 if not isinstance(options, dict): 

1232 raise JenkinsJobException( # pragma: no cover 

1233 "The last element of the tuple must be a dictionary:\nJOB:\n" + str(options)) 

1234 else: 

1235 options = {} 

1236 

1237 # job and scheduler 

1238 job, scheduler_options = job[:2] 

1239 if isinstance(scheduler_options, dict): 

1240 scheduler = scheduler_options.get('scheduler', None) 

1241 else: 

1242 scheduler = scheduler_options 

1243 scheduler_options = None 

1244 if scheduler is not None: 

1245 order = 1 

1246 if counts.get(dozen, 0) > 0: 

1247 dozen = chr(ord(dozen) + 1) 

1248 else: 

1249 if i == 0: 

1250 order += 1 

1251 else: 

1252 scheduler = None 

1253 if i == 0: 

1254 order += 1 

1255 options = {} 

1256 is_yml = False 

1257 

1258 # all schedule are disabled if disable_schedule is True 

1259 if disable_schedule: 

1260 scheduler = None 

1261 counts[dozen] = counts.get(dozen, 0) + 1 

1262 

1263 # success_only 

1264 if "success_only" in options: 

1265 success_only = options["success_only"] 

1266 del options["success_only"] 

1267 else: 

1268 success_only = False 

1269 

1270 # timeout 

1271 if "timeout" in options: 

1272 timeout = options["timeout"] 

1273 del options["timeout"] 

1274 else: 

1275 timeout = _timeout_default 

1276 

1277 # script 

1278 if not is_yml: 

1279 script = get_jenkins_script(self, job) 

1280 

1281 # we process the repository 

1282 if "repo" in options: 

1283 gitrepo = options["repo"] 

1284 options = options.copy() 

1285 del options["repo"] 

1286 else: 

1287 gitrepo = github 

1288 

1289 # add a description to the job 

1290 description = ["%s%02d%02d" % (dozen, order, unit)] 

1291 if scheduler is not None: 

1292 description.append(scheduler) 

1293 try: 

1294 description = " - ".join(description) 

1295 except TypeError as e: # pragma: no cover 

1296 raise TypeError("Issue with {}.".format(description)) from e 

1297 

1298 # credentials 

1299 if isinstance(credentials, dict): # pragma: no cover 

1300 cred = credentials.get(gitrepo, None) 

1301 if cred is None: 

1302 cred = credentials.get("default", "") 

1303 else: 

1304 cred = credentials 

1305 

1306 if not is_yml: 

1307 mod = job.split()[0] 

1308 name = self.get_jenkins_job_name(job) 

1309 jname = prefix + name 

1310 

1311 try: 

1312 j = jenkins_server.get_job_config( 

1313 jname) if not jenkins_server._mock else None 

1314 except jenkins.NotFoundException: # pragma: no cover 

1315 j = None 

1316 except jenkins.JenkinsException as e: # pragma: no cover 

1317 raise JenkinsExtException( 

1318 "unable to retrieve job config for job={0}, name={1}".format(job, jname)) from e 

1319 

1320 if overwrite or j is None: 

1321 

1322 update_job = False 

1323 if j is not None: # pragma: no cover 

1324 if update: 

1325 update_job = True 

1326 else: 

1327 self.fLOG("[jenkins] delete job", jname) 

1328 jenkins_server.delete_job(jname) 

1329 

1330 # we post process the script 

1331 script = self.process_options(script, options) 

1332 

1333 # if there is a script 

1334 if script is not None and len(script) > 0: 

1335 new_dep.append(name) 

1336 upstreams = [] if ( 

1337 scheduler is not None) else deps[-1:] 

1338 self.fLOG("[jenkins] create job", jname, " - ", job, 

1339 " : ", scheduler, " / ", upstreams) 

1340 

1341 # set up location 

1342 if location is None: 

1343 loc = None # pragma: no cover 

1344 else: 

1345 if "_" in jname: 

1346 loc = os.path.join(location, name, jname) 

1347 else: 

1348 loc = os.path.join(location, name, "_" + jname) 

1349 

1350 if mod in ("standalone", "custom"): 

1351 gpar = None 

1352 elif gitrepo is None: 

1353 raise JenkinsJobException( # pragma: no cover 

1354 "gitrepo cannot must not be None if standalone or " 

1355 "custom is not defined,\njob=" + str(job)) 

1356 elif gitrepo.endswith(".git"): 

1357 gpar = gitrepo 

1358 else: 

1359 gpar = gitrepo + "%s/" % mod 

1360 

1361 # create the template 

1362 r = jenkins_server.create_job_template(jname, git_repo=gpar, upstreams=upstreams, script=script, 

1363 location=loc, scheduler=scheduler, py27="[27]" in job, 

1364 description=description, credentials=cred, success_only=success_only, 

1365 update=update_job, timeout=timeout, adjust_scheduler=adjust_scheduler, 

1366 mails=self.mails) 

1367 

1368 # check some inconsistencies 

1369 if "[27]" in job and "Anaconda3" in script: 

1370 raise JenkinsExtException( # pragma: no cover 

1371 "incoherence for job {0}, script:\n{1}".format(job, script)) 

1372 

1373 locations.append((job, loc)) 

1374 created.append((job, name, loc, job, r)) 

1375 else: # pragma: no cover 

1376 # skip the job 

1377 loc = None if location is None else os.path.join( 

1378 location, jname) 

1379 locations.append((job, loc)) 

1380 self.fLOG("[jenkins] skipping", 

1381 job, "location", loc) 

1382 elif j is not None: 

1383 new_dep.append(name) 

1384 

1385 else: 

1386 # yml file 

1387 if location is not None: 

1388 options["root_path"] = location 

1389 for k, v in self.engines.items(): 

1390 if k not in options: 

1391 options[k] = v 

1392 jobdef = job[0] if isinstance(job, tuple) else job 

1393 

1394 done = {} 

1395 for aj, name, var in enumerate_processed_yml( 

1396 jobdef, context=options, engine=yml_engine, 

1397 add_environ=add_environ, server=self, git_repo=gitrepo, 

1398 scheduler=scheduler, description=description, credentials=cred, 

1399 success_only=success_only, timeout=timeout, platform=self.platform, 

1400 adjust_scheduler=adjust_scheduler, overwrite=overwrite, 

1401 build_location=location, mails=self.mails, 

1402 job_options=scheduler_options): 

1403 if name in done: 

1404 s = "A name '{0}' was already used for a job, from:\n{1}\nPROCESS:\n{2}" # pragma: no cover 

1405 raise ValueError( # pragma: no cover 

1406 s.format(name, jobdef, "\n".join(sorted(set(done.keys()))))) 

1407 done[name] = (aj, name, var) 

1408 loc = None if location is None else os.path.join( 

1409 location, name) 

1410 self.fLOG("[jenkins] adding i={2}: '{0}' var='{1}'".format( 

1411 name, var, len(created))) 

1412 created.append((job, name, loc, job, aj)) 

1413 

1414 indexes["order"] = order 

1415 indexes["dozen"] = dozen 

1416 indexes["unit"] = unit 

1417 return created, new_dep, locations