Commit | Line | Data |
---|---|---|
962ee225 FD |
1 | /** |
2 | * Copyright (C) 2017 - Francis Deslauriers <francis.deslauriers@efficios.com> | |
3 | * | |
4 | * This program is free software: you can redistribute it and/or modify | |
5 | * it under the terms of the GNU General Public License as published by | |
6 | * the Free Software Foundation, either version 3 of the License, or | |
7 | * (at your option) any later version. | |
8 | * | |
9 | * This program is distributed in the hope that it will be useful, | |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | * GNU General Public License for more details. | |
13 | * | |
14 | * You should have received a copy of the GNU General Public License | |
15 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
16 | */ | |
17 | ||
18 | import hudson.console.HyperlinkNote | |
19 | import hudson.model.* | |
20 | import java.io.File | |
21 | import org.eclipse.jgit.api.Git | |
22 | import org.eclipse.jgit.lib.Ref | |
23 | ||
24 | class InvalidKVersionException extends Exception { | |
25 | public InvalidKVersionException(String message) { | |
26 | super(message) | |
27 | } | |
28 | } | |
29 | ||
30 | class EmptyKVersionException extends Exception { | |
31 | public EmptyKVersionException(String message) { | |
32 | super(message) | |
33 | } | |
34 | } | |
35 | ||
36 | class VanillaKVersion implements Comparable<VanillaKVersion> { | |
37 | ||
38 | Integer major = 0 | |
39 | Integer majorB = 0 | |
40 | Integer minor = 0 | |
41 | Integer patch = 0 | |
42 | Integer rc = Integer.MAX_VALUE | |
43 | Boolean inStable = false; | |
44 | ||
45 | VanillaKVersion() {} | |
46 | ||
47 | VanillaKVersion(version) { | |
48 | this.parse(version) | |
49 | } | |
50 | ||
51 | static VanillaKVersion minKVersion() { | |
52 | return new VanillaKVersion("v0.0.0") | |
53 | } | |
54 | ||
55 | static VanillaKVersion maxKVersion() { | |
56 | return new VanillaKVersion("v" + Integer.MAX_VALUE + ".0.0") | |
57 | } | |
58 | ||
59 | static VanillaKVersion factory(version) { | |
60 | return new VanillaKVersion(version) | |
61 | } | |
62 | ||
63 | def parse(version) { | |
64 | this.major = 0 | |
65 | this.majorB = 0 | |
66 | this.minor = 0 | |
67 | this.patch = 0 | |
68 | this.rc = Integer.MAX_VALUE | |
69 | ||
70 | if (!version) { | |
71 | throw new EmptyKVersionException("Empty kernel version") | |
72 | } | |
73 | ||
74 | def match = version =~ /^v(\d+)\.(\d+)(\.(\d+))?(\.(\d+))?(-rc(\d+))?$/ | |
75 | if (!match) { | |
76 | throw new InvalidKVersionException("Invalid kernel version: ${version}") | |
77 | } | |
78 | ||
79 | Integer offset = 0; | |
80 | ||
81 | // Major | |
82 | this.major = Integer.parseInt(match.group(1)) | |
83 | if (this.major <= 2) { | |
84 | offset = 2 | |
85 | this.majorB = Integer.parseInt(match.group(2)) | |
86 | } | |
87 | ||
88 | // Minor | |
89 | if (match.group(2 + offset) != null) { | |
90 | this.minor = Integer.parseInt(match.group(2 + offset)) | |
91 | } | |
92 | ||
93 | // Patch level | |
94 | if (match.group(4 + offset) != null) { | |
95 | this.patch = Integer.parseInt(match.group(4 + offset)) | |
96 | this.inStable = true | |
97 | } | |
98 | ||
99 | // RC | |
100 | if (match.group(8) != null) { | |
101 | this.rc = Integer.parseInt(match.group(8)) | |
102 | } | |
103 | } | |
104 | ||
105 | Boolean isInStableBranch() { | |
106 | return this.inStable | |
107 | } | |
108 | ||
109 | // Return true if both version are of the same stable branch | |
110 | Boolean isSameStable(VanillaKVersion o) { | |
111 | if (this.major != o.major) { | |
112 | return false | |
113 | } | |
114 | if (this.majorB != o.majorB) { | |
115 | return false | |
116 | } | |
117 | if (this.minor != o.minor) { | |
118 | return false | |
119 | } | |
120 | ||
121 | return true | |
122 | } | |
123 | ||
124 | @Override int compareTo(VanillaKVersion o) { | |
125 | if (this.major != o.major) { | |
126 | return Integer.compare(this.major, o.major) | |
127 | } | |
128 | if (this.majorB != o.majorB) { | |
129 | return Integer.compare(this.majorB, o.majorB) | |
130 | } | |
131 | if (this.minor != o.minor) { | |
132 | return Integer.compare(this.minor, o.minor) | |
133 | } | |
134 | if (this.patch != o.patch) { | |
135 | return Integer.compare(this.patch, o.patch) | |
136 | } | |
137 | if (this.rc != o.rc) { | |
138 | return Integer.compare(this.rc, o.rc) | |
139 | } | |
140 | ||
141 | // Same version | |
142 | return 0; | |
143 | } | |
144 | ||
145 | String toString() { | |
146 | String vString = "v${this.major}" | |
147 | ||
148 | if (this.majorB > 0) { | |
149 | vString = vString.concat(".${this.majorB}") | |
150 | } | |
151 | ||
152 | vString = vString.concat(".${this.minor}") | |
153 | ||
154 | if (this.patch > 0) { | |
155 | vString = vString.concat(".${this.patch}") | |
156 | } | |
157 | ||
158 | if (this.rc > 0 && this.rc < Integer.MAX_VALUE) { | |
159 | vString = vString.concat("-rc${this.rc}") | |
160 | } | |
161 | return vString | |
162 | } | |
163 | } | |
164 | ||
802e75a7 FD |
165 | // Save the hashmap containing all the jobs and their status to disk. We can do |
166 | // that because this job is configured to always run on the master node on | |
167 | // Jenkins. | |
168 | def SaveCurrentJobsToWorkspace = { currentJobs, ondiskpath-> | |
962ee225 FD |
169 | try { |
170 | File myFile = new File(ondiskpath); | |
802e75a7 FD |
171 | myFile.createNewFile(); |
172 | def out = new ObjectOutputStream(new FileOutputStream(ondiskpath)) | |
173 | out.writeObject(currentJobs) | |
174 | out.close() | |
5a754cf7 | 175 | } catch (e) { |
802e75a7 | 176 | println("Failed to save previous Git object IDs to disk." + e); |
962ee225 | 177 | } |
962ee225 FD |
178 | } |
179 | ||
802e75a7 FD |
180 | // Load the hashmap containing all the jobs and their last status from disk. |
181 | // It's possible because this job is configured to always run on the master | |
182 | // node on Jenkins | |
183 | def LoadPreviousJobsFromWorkspace = { ondiskpath -> | |
184 | def previousJobs = [:] | |
962ee225 FD |
185 | try { |
186 | File myFile = new File(ondiskpath); | |
802e75a7 FD |
187 | def input = new ObjectInputStream(new FileInputStream(ondiskpath)) |
188 | previousJobs = input.readObject() | |
189 | input.close() | |
5a754cf7 | 190 | } catch (e) { |
802e75a7 | 191 | println("Failed to load previous runs from disk." + e); |
962ee225 | 192 | } |
802e75a7 | 193 | return previousJobs |
962ee225 FD |
194 | } |
195 | ||
802e75a7 | 196 | |
962ee225 FD |
197 | def GetHeadCommits = { remoteRepo, branchesOfInterest -> |
198 | def remoteHeads = [:] | |
199 | def remoteHeadRefs = Git.lsRemoteRepository() | |
200 | .setTags(false) | |
201 | .setHeads(true) | |
202 | .setRemote(remoteRepo).call() | |
203 | ||
204 | remoteHeadRefs.each { | |
205 | def branch = it.getName().replaceAll('refs/heads/', '') | |
206 | if (branchesOfInterest.contains(branch)) | |
207 | remoteHeads[branch] = it.getObjectId().name() | |
208 | } | |
209 | ||
210 | return remoteHeads | |
211 | } | |
212 | ||
213 | def GetTagIds = { remoteRepo -> | |
214 | def remoteTags = [:] | |
215 | def remoteTagRefs = Git.lsRemoteRepository() | |
216 | .setTags(true) | |
217 | .setHeads(false) | |
218 | .setRemote(remoteRepo).call() | |
219 | ||
220 | remoteTagRefs.each { | |
221 | // Exclude release candidate tags | |
222 | if (!it.getName().contains('-rc')) { | |
223 | remoteTags[it.getName().replaceAll('refs/tags/', '')] = it.getObjectId().name() | |
224 | } | |
225 | } | |
226 | ||
227 | return remoteTags | |
228 | } | |
229 | ||
230 | def GetLastTagOfBranch = { tagRefs, branch -> | |
231 | def tagVersions = tagRefs.collect {new VanillaKVersion(it.key)} | |
232 | def currMax = new VanillaKVersion('v0.0.0'); | |
233 | if (!branch.contains('master')){ | |
234 | def targetVersion = new VanillaKVersion(branch.replaceAll('linux-', 'v').replaceAll('.y', '')) | |
235 | tagVersions.each { | |
236 | if (it.isSameStable(targetVersion)) { | |
237 | if (currMax < it) { | |
238 | currMax = it; | |
239 | } | |
240 | } | |
241 | } | |
242 | } else { | |
243 | tagVersions.each { | |
244 | if (!it.isInStableBranch() && currMax < it) { | |
245 | currMax = it; | |
246 | } | |
247 | } | |
248 | } | |
249 | return currMax.toString() | |
250 | } | |
251 | ||
252 | // Returns the latest tags of each of the branches passed in the argument | |
253 | def GetLastTagIds = { remoteRepo, branchesOfInterest -> | |
254 | def remoteHeads = GetHeadCommits(remoteRepo, branchesOfInterest) | |
255 | def remoteTagRefs = GetTagIds(remoteRepo) | |
256 | def remoteLastTagCommit = [:] | |
257 | ||
258 | remoteTagRefs = remoteTagRefs.findAll { !it.key.contains("v2.") } | |
259 | branchesOfInterest.each { | |
260 | remoteLastTagCommit[it] = remoteTagRefs[GetLastTagOfBranch(remoteTagRefs, it)] | |
261 | } | |
262 | ||
263 | return remoteLastTagCommit | |
264 | } | |
265 | ||
802e75a7 FD |
266 | def CraftJobName = { jobType, linuxBranch, lttngBranch -> |
267 | return "${jobType}_k${linuxBranch}_l${lttngBranch}" | |
962ee225 FD |
268 | } |
269 | ||
802e75a7 | 270 | def LaunchJob = { jobName, jobInfo -> |
962ee225 FD |
271 | def job = Hudson.instance.getJob(jobName) |
272 | def params = [] | |
273 | for (paramdef in job.getProperty(ParametersDefinitionProperty.class).getParameterDefinitions()) { | |
0d4a7f6b FD |
274 | // If there is a default value for this parameter, use it. Don't use empty |
275 | // default value parameters. | |
276 | if (paramdef.getDefaultValue()) { | |
277 | params += paramdef.getDefaultParameterValue(); | |
278 | } | |
962ee225 FD |
279 | } |
280 | ||
802e75a7 FD |
281 | params.add(new StringParameterValue('LTTNG_TOOLS_COMMIT_ID', jobInfo['config']['toolsCommit'])) |
282 | params.add(new StringParameterValue('LTTNG_MODULES_COMMIT_ID', jobInfo['config']['modulesCommit'])) | |
283 | params.add(new StringParameterValue('LTTNG_UST_COMMIT_ID', jobInfo['config']['ustCommit'])) | |
284 | params.add(new StringParameterValue('KERNEL_TAG_ID', jobInfo['config']['linuxTagID'])) | |
5a754cf7 FD |
285 | def currBuild = job.scheduleBuild2(0, new Cause.UpstreamCause(build), new ParametersAction(params)) |
286 | ||
287 | if (currBuild != null ) { | |
288 | println("Launching job: ${HyperlinkNote.encodeTo('/' + job.url, job.fullDisplayName)}"); | |
289 | } else { | |
290 | println("Job ${jobName} not found or deactivated."); | |
291 | } | |
292 | ||
293 | return currBuild | |
962ee225 FD |
294 | } |
295 | ||
962ee225 FD |
296 | final String toolsRepo = "https://github.com/lttng/lttng-tools.git" |
297 | final String modulesRepo = "https://github.com/lttng/lttng-modules.git" | |
298 | final String ustRepo = "https://github.com/lttng/lttng-ust.git" | |
299 | final String linuxRepo = "git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git" | |
300 | ||
802e75a7 | 301 | final String pastJobsPath = build.getEnvironment(listener).get('WORKSPACE') + "/pastjobs"; |
962ee225 FD |
302 | |
303 | def recentLttngBranchesOfInterest = ['master', 'stable-2.10', 'stable-2.9'] | |
304 | def recentLinuxBranchesOfInterest = ['master', 'linux-4.9.y', 'linux-4.4.y'] | |
305 | ||
306 | def legacyLttngBranchesOfInterest = ['stable-2.7'] | |
49499ecb | 307 | def legacyLinuxBranchesOfInterest = ['linux-3.18.y'] |
962ee225 | 308 | |
ca4d4c72 | 309 | // Generate configurations of interest. |
962ee225 FD |
310 | def configurationOfInterest = [] as Set |
311 | ||
312 | recentLttngBranchesOfInterest.each { lttngBranch -> | |
313 | recentLinuxBranchesOfInterest.each { linuxBranch -> | |
314 | configurationOfInterest.add([lttngBranch, linuxBranch]) | |
315 | } | |
316 | } | |
317 | ||
318 | legacyLttngBranchesOfInterest.each { lttngBranch -> | |
319 | legacyLinuxBranchesOfInterest.each { linuxBranch -> | |
320 | configurationOfInterest.add([lttngBranch, linuxBranch]) | |
321 | } | |
322 | } | |
323 | ||
324 | def lttngBranchesOfInterest = recentLttngBranchesOfInterest + legacyLttngBranchesOfInterest | |
325 | def linuxBranchesOfInterest = recentLinuxBranchesOfInterest + legacyLinuxBranchesOfInterest | |
326 | ||
ca4d4c72 | 327 | // For LTTng branches, we look for new commits. |
962ee225 FD |
328 | def toolsHeadCommits = GetHeadCommits(toolsRepo, lttngBranchesOfInterest) |
329 | def modulesHeadCommits = GetHeadCommits(modulesRepo, lttngBranchesOfInterest) | |
330 | def ustHeadCommits = GetHeadCommits(ustRepo, lttngBranchesOfInterest) | |
331 | ||
ca4d4c72 | 332 | // For Linux branches, we look for new non-RC tags. |
962ee225 FD |
333 | def linuxLastTagIds = GetLastTagIds(linuxRepo, linuxBranchesOfInterest) |
334 | ||
802e75a7 FD |
335 | def CraftConfig = { linuxBr, lttngBr -> |
336 | def job = [:]; | |
337 | job['config'] = [:]; | |
338 | job['config']['linuxBranch'] = linuxBr; | |
339 | job['config']['lttngBranch'] = lttngBr; | |
340 | job['config']['linuxTagID'] = linuxLastTagIds[linuxBr]; | |
341 | job['config']['toolsCommit'] = toolsHeadCommits[lttngBr]; | |
342 | job['config']['modulesCommit'] = modulesHeadCommits[lttngBr]; | |
343 | job['config']['ustCommit'] = ustHeadCommits[lttngBr]; | |
344 | job['status'] = 'NOT_SET'; | |
345 | job['build'] = null; | |
346 | return job; | |
962ee225 FD |
347 | } |
348 | ||
5a754cf7 FD |
349 | // Check what type of jobs should be triggered. |
350 | triggerJobName = build.project.getFullDisplayName(); | |
351 | if (triggerJobName.contains("vm_tests")) { | |
352 | jobType = 'vm_tests'; | |
353 | } else if (triggerJobName.contains("baremetal_tests")) { | |
354 | jobType = 'baremetal_tests'; | |
355 | } else if (triggerJobName.contains("baremetal_benchmarks")) { | |
356 | jobType = 'baremetal_benchmarks'; | |
357 | } | |
962ee225 | 358 | |
802e75a7 FD |
359 | // Hashmap containing all the jobs, their configuration (commit id, etc. )and |
360 | // their status (SUCCEEDED, FAILED, etc.). This Hashmap is made of basic strings | |
361 | // rather than objects and enums because strings are easily serializable. | |
362 | def currentJobs = [:]; | |
5a754cf7 | 363 | |
802e75a7 FD |
364 | // Get an up to date view of all the branches of interest. |
365 | configurationOfInterest.each { lttngBr, linuxBr -> | |
366 | def jobName = CraftJobName(jobType, linuxBr, lttngBr); | |
367 | currentJobs[jobName] = CraftConfig(linuxBr, lttngBr); | |
962ee225 | 368 | |
802e75a7 FD |
369 | // Add fuzzing job in vm_tests on master branches of lttng and linux. |
370 | if (jobType == 'vm_tests' && lttngBr == 'master' && linuxBr == 'master') { | |
371 | def vmFuzzingJobName = CraftJobName(jobType + '_fuzzing', linuxBr, lttngBr); | |
372 | currentJobs[vmFuzzingJobName] = CraftConfig(linuxBr, lttngBr); | |
962ee225 | 373 | } |
962ee225 | 374 | } |
5a754cf7 | 375 | |
802e75a7 FD |
376 | //Add canary job |
377 | def jobNameCanary = jobType + "_canary"; | |
378 | currentJobs[jobNameCanary] = [:]; | |
379 | currentJobs[jobNameCanary]['config'] = [:]; | |
380 | currentJobs[jobNameCanary]['config']['linuxBranch'] = 'v4.4.9'; | |
381 | currentJobs[jobNameCanary]['config']['lttngBranch'] = 'v2.8.1'; | |
382 | currentJobs[jobNameCanary]['config']['linuxTagID'] ='1a1a512b983108015ced1e7a7c7775cfeec42d8c'; | |
383 | currentJobs[jobNameCanary]['config']['toolsCommit'] = 'd11e0db' | |
384 | currentJobs[jobNameCanary]['config']['modulesCommit'] = '7fd9215' | |
385 | currentJobs[jobNameCanary]['config']['ustCommit'] = '514a87f' | |
386 | currentJobs[jobNameCanary]['status'] = 'NOT_SET'; | |
387 | currentJobs[jobNameCanary]['build'] = null; | |
388 | ||
389 | def pastJobs = LoadPreviousJobsFromWorkspace(pastJobsPath); | |
112ef919 | 390 | |
802e75a7 FD |
391 | def failedRuns = [] |
392 | def abortedRuns = [] | |
393 | def isFailed = false | |
394 | def isAborted = false | |
395 | def ongoingJobs = 0; | |
396 | ||
397 | currentJobs.each { jobName, jobInfo -> | |
398 | // If the job ran in the past, we check if the IDs changed since. | |
399 | if (pastJobs.containsKey(jobName) && !jobName.contains('_canary')) { | |
400 | pastJob = pastJobs[jobName]; | |
401 | // Have the IDs changed? | |
402 | if (pastJob['config'] == jobInfo['config']) { | |
403 | // if the config has not changed, we keep it. | |
404 | // if it's failed, we don't launch a new job and keep it failed. | |
405 | jobInfo['status'] = pastJob['status']; | |
406 | if (pastJob['status'] == 'FAILED') { | |
407 | println("${jobName} as not changed since the last failed run. Don't run it again."); | |
408 | // Marked the umbrella job for failure but still run the jobs that since the | |
409 | // last run. | |
410 | isFailed = true; | |
411 | return; | |
412 | } else if (pastJob['status'] == 'ABORTED') { | |
413 | println("${jobName} as not changed since last aborted run. Run it again."); | |
414 | } else if (pastJob['status'] == 'SUCCEEDED') { | |
415 | println("${jobName} as not changed since the last successful run. Don't run it again."); | |
416 | return; | |
417 | } | |
418 | } | |
112ef919 | 419 | } |
802e75a7 FD |
420 | |
421 | jobInfo['status'] = 'PENDING'; | |
422 | jobInfo['build'] = LaunchJob(jobName, jobInfo); | |
423 | ongoingJobs += 1; | |
112ef919 FD |
424 | } |
425 | ||
802e75a7 FD |
426 | while (ongoingJobs > 0) { |
427 | currentJobs.each { jobName, jobInfo -> | |
428 | ||
429 | if (jobInfo['status'] != 'PENDING') { | |
430 | return; | |
431 | } | |
432 | ||
433 | jobBuild = jobInfo['build'] | |
5a754cf7 FD |
434 | |
435 | // The isCancelled() method checks if the run was cancelled before | |
436 | // execution. We consider such run as being aborted. | |
802e75a7 | 437 | if (jobBuild.isCancelled()) { |
5a754cf7 | 438 | println("${jobName} was cancelled before launch.") |
5a754cf7 | 439 | isAborted = true; |
802e75a7 FD |
440 | abortedRuns.add(jobName); |
441 | ongoingJobs -= 1; | |
442 | jobInfo['status'] = 'ABORTED' | |
443 | // Invalidate the build field, as it's not serializable and we don't need | |
444 | // it anymore. | |
445 | jobInfo['build'] = null; | |
446 | } else if (jobBuild.isDone()) { | |
447 | ||
448 | jobExitStatus = jobBuild.get(); | |
5a754cf7 | 449 | |
802e75a7 FD |
450 | // Invalidate the build field, as it's not serializable and we don't need |
451 | // it anymore. | |
452 | jobInfo['build'] = null; | |
453 | println("${jobExitStatus.fullDisplayName} completed with status ${jobExitStatus.result}."); | |
5a754cf7 FD |
454 | |
455 | // If the job didn't succeed, add its name to the right list so it can | |
456 | // be printed at the end of the execution. | |
802e75a7 FD |
457 | ongoingJobs -= 1; |
458 | switch (jobExitStatus.result) { | |
5a754cf7 FD |
459 | case Result.ABORTED: |
460 | isAborted = true; | |
461 | abortedRuns.add(jobName); | |
802e75a7 | 462 | jobInfo['status'] = 'ABORTED' |
5a754cf7 FD |
463 | break; |
464 | case Result.FAILURE: | |
465 | isFailed = true; | |
466 | failedRuns.add(jobName); | |
802e75a7 | 467 | jobInfo['status'] = 'FAILED' |
5a754cf7 FD |
468 | break; |
469 | case Result.SUCCESS: | |
802e75a7 FD |
470 | jobInfo['status'] = 'SUCCEEDED' |
471 | break; | |
5a754cf7 FD |
472 | default: |
473 | break; | |
474 | } | |
5a754cf7 FD |
475 | } |
476 | } | |
477 | ||
478 | // Sleep before the next iteration. | |
479 | try { | |
b2c3f97c | 480 | Thread.sleep(30000) |
5a754cf7 FD |
481 | } catch(e) { |
482 | if (e in InterruptedException) { | |
483 | build.setResult(hudson.model.Result.ABORTED) | |
484 | throw new InterruptedException() | |
485 | } else { | |
486 | throw(e) | |
487 | } | |
488 | } | |
489 | } | |
490 | ||
802e75a7 FD |
491 | //All jobs are done running. Save their exit status to disk. |
492 | SaveCurrentJobsToWorkspace(currentJobs, pastJobsPath); | |
493 | ||
5a754cf7 FD |
494 | // Get log of failed runs. |
495 | if (failedRuns.size() > 0) { | |
496 | println("Failed job(s):"); | |
497 | for (failedRun in failedRuns) { | |
498 | println("\t" + failedRun) | |
499 | } | |
500 | } | |
501 | ||
502 | // Get log of aborted runs. | |
503 | if (abortedRuns.size() > 0) { | |
504 | println("Cancelled job(s):"); | |
505 | for (cancelledRun in abortedRuns) { | |
506 | println("\t" + cancelledRun) | |
507 | } | |
508 | } | |
509 | ||
510 | // Mark this build as Failed if atleast one child build has failed and mark as | |
511 | // aborted if there was no failure but atleast one job aborted. | |
512 | if (isFailed) { | |
513 | build.setResult(hudson.model.Result.FAILURE) | |
514 | } else if (isAborted) { | |
515 | build.setResult(hudson.model.Result.ABORTED) | |
516 | } |