ansible: Support Ubuntu in docker role
[lttng-ci.git] / scripts / system-tests / system-trigger.groovy
CommitLineData
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
18import hudson.console.HyperlinkNote
19import hudson.model.*
20import java.io.File
21import org.eclipse.jgit.api.Git
22import org.eclipse.jgit.lib.Ref
23
24class InvalidKVersionException extends Exception {
25 public InvalidKVersionException(String message) {
26 super(message)
27 }
28}
29
30class EmptyKVersionException extends Exception {
31 public EmptyKVersionException(String message) {
32 super(message)
33 }
34}
35
36class 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.
168def 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
183def 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
197def 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
213def 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
230def 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
253def 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
266def CraftJobName = { jobType, linuxBranch, lttngBranch ->
267 return "${jobType}_k${linuxBranch}_l${lttngBranch}"
962ee225
FD
268}
269
802e75a7 270def LaunchJob = { jobName, jobInfo ->
962ee225 271 def job = Hudson.instance.getJob(jobName)
9ee19c2b
KS
272 if (job == null) {
273 println(String.format("Failed to find job by name '%s'", jobName))
274 return null;
275 }
962ee225
FD
276 def params = []
277 for (paramdef in job.getProperty(ParametersDefinitionProperty.class).getParameterDefinitions()) {
0d4a7f6b
FD
278 // If there is a default value for this parameter, use it. Don't use empty
279 // default value parameters.
c500f461 280 if (paramdef.getDefaultParameterValue() != null) {
0d4a7f6b
FD
281 params += paramdef.getDefaultParameterValue();
282 }
962ee225
FD
283 }
284
802e75a7
FD
285 params.add(new StringParameterValue('LTTNG_TOOLS_COMMIT_ID', jobInfo['config']['toolsCommit']))
286 params.add(new StringParameterValue('LTTNG_MODULES_COMMIT_ID', jobInfo['config']['modulesCommit']))
287 params.add(new StringParameterValue('LTTNG_UST_COMMIT_ID', jobInfo['config']['ustCommit']))
288 params.add(new StringParameterValue('KERNEL_TAG_ID', jobInfo['config']['linuxTagID']))
5a754cf7
FD
289 def currBuild = job.scheduleBuild2(0, new Cause.UpstreamCause(build), new ParametersAction(params))
290
291 if (currBuild != null ) {
292 println("Launching job: ${HyperlinkNote.encodeTo('/' + job.url, job.fullDisplayName)}");
293 } else {
294 println("Job ${jobName} not found or deactivated.");
295 }
296
297 return currBuild
962ee225
FD
298}
299
962ee225
FD
300final String toolsRepo = "https://github.com/lttng/lttng-tools.git"
301final String modulesRepo = "https://github.com/lttng/lttng-modules.git"
302final String ustRepo = "https://github.com/lttng/lttng-ust.git"
303final String linuxRepo = "git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git"
304
802e75a7 305final String pastJobsPath = build.getEnvironment(listener).get('WORKSPACE') + "/pastjobs";
962ee225 306
920a3cfd 307def recentLttngBranchesOfInterest = ['master',
bcb99e25
JR
308 'stable-2.13',
309 'stable-2.12']
2537aa57 310def recentLinuxBranchesOfInterest = ['master',
30052d66 311 'linux-6.6.y',
d8858d79 312 'linux-6.1.y',
bcb99e25
JR
313 'linux-5.15.y',
314 'linux-5.10.y',
942c3046 315 'linux-5.4.y',
2537aa57
JR
316 'linux-4.19.y',
317 'linux-4.14.y',
d8858d79 318]
962ee225 319
3f0881e9 320def legacyLttngBranchesOfInterest = []
d8858d79
KS
321def legacyLinuxBranchesOfInterest = [
322 'linux-5.14.y',
323 'linux-4.18.y',
324 'linux-4.12.y',
325 'linux-4.9.y',
326]
327
328def vmLinuxBranchesOfInterest = []
a28d0f54 329
ca4d4c72 330// Generate configurations of interest.
962ee225
FD
331def configurationOfInterest = [] as Set
332
333recentLttngBranchesOfInterest.each { lttngBranch ->
334 recentLinuxBranchesOfInterest.each { linuxBranch ->
335 configurationOfInterest.add([lttngBranch, linuxBranch])
336 }
337}
338
339legacyLttngBranchesOfInterest.each { lttngBranch ->
340 legacyLinuxBranchesOfInterest.each { linuxBranch ->
341 configurationOfInterest.add([lttngBranch, linuxBranch])
342 }
343}
344
345def lttngBranchesOfInterest = recentLttngBranchesOfInterest + legacyLttngBranchesOfInterest
edddabaa 346def linuxBranchesOfInterest = recentLinuxBranchesOfInterest + legacyLinuxBranchesOfInterest + vmLinuxBranchesOfInterest
962ee225 347
ca4d4c72 348// For LTTng branches, we look for new commits.
962ee225
FD
349def toolsHeadCommits = GetHeadCommits(toolsRepo, lttngBranchesOfInterest)
350def modulesHeadCommits = GetHeadCommits(modulesRepo, lttngBranchesOfInterest)
351def ustHeadCommits = GetHeadCommits(ustRepo, lttngBranchesOfInterest)
352
ca4d4c72 353// For Linux branches, we look for new non-RC tags.
962ee225
FD
354def linuxLastTagIds = GetLastTagIds(linuxRepo, linuxBranchesOfInterest)
355
802e75a7
FD
356def CraftConfig = { linuxBr, lttngBr ->
357 def job = [:];
358 job['config'] = [:];
359 job['config']['linuxBranch'] = linuxBr;
360 job['config']['lttngBranch'] = lttngBr;
361 job['config']['linuxTagID'] = linuxLastTagIds[linuxBr];
362 job['config']['toolsCommit'] = toolsHeadCommits[lttngBr];
363 job['config']['modulesCommit'] = modulesHeadCommits[lttngBr];
364 job['config']['ustCommit'] = ustHeadCommits[lttngBr];
365 job['status'] = 'NOT_SET';
366 job['build'] = null;
367 return job;
962ee225
FD
368}
369
5a754cf7
FD
370// Check what type of jobs should be triggered.
371triggerJobName = build.project.getFullDisplayName();
372if (triggerJobName.contains("vm_tests")) {
373 jobType = 'vm_tests';
a28d0f54
JR
374 recentLttngBranchesOfInterest.each { lttngBranch ->
375 vmLinuxBranchesOfInterest.each { linuxBranch ->
376 configurationOfInterest.add([lttngBranch, linuxBranch])
377 }
378 }
5a754cf7
FD
379} else if (triggerJobName.contains("baremetal_tests")) {
380 jobType = 'baremetal_tests';
5a754cf7 381}
962ee225 382
802e75a7
FD
383// Hashmap containing all the jobs, their configuration (commit id, etc. )and
384// their status (SUCCEEDED, FAILED, etc.). This Hashmap is made of basic strings
385// rather than objects and enums because strings are easily serializable.
386def currentJobs = [:];
5a754cf7 387
802e75a7
FD
388// Get an up to date view of all the branches of interest.
389configurationOfInterest.each { lttngBr, linuxBr ->
390 def jobName = CraftJobName(jobType, linuxBr, lttngBr);
391 currentJobs[jobName] = CraftConfig(linuxBr, lttngBr);
962ee225 392}
5a754cf7 393
802e75a7 394//Add canary job
c17a93f3 395def jobNameCanary = jobType + "_kcanary_lcanary";
802e75a7
FD
396currentJobs[jobNameCanary] = [:];
397currentJobs[jobNameCanary]['config'] = [:];
26990b2f
KS
398currentJobs[jobNameCanary]['config']['linuxBranch'] = 'v5.15.112';
399currentJobs[jobNameCanary]['config']['lttngBranch'] = 'v2.13.9';
400currentJobs[jobNameCanary]['config']['linuxTagID'] ='9d6bde853685609a631871d7c12be94fdf8d912e'; // v5.15.112
401currentJobs[jobNameCanary]['config']['toolsCommit'] = '2ff0385718ff894b3d0e06f3961334c20c5436f8' // v2.13.9
402currentJobs[jobNameCanary]['config']['modulesCommit'] = 'da1f5a264fff33fc5a9518e519fb0084bf1074af' // v2.13.9
403currentJobs[jobNameCanary]['config']['ustCommit'] = 'de624c20694f69702b42c5d47b5bcf692293a238' // v2.13.5
802e75a7
FD
404currentJobs[jobNameCanary]['status'] = 'NOT_SET';
405currentJobs[jobNameCanary]['build'] = null;
406
407def pastJobs = LoadPreviousJobsFromWorkspace(pastJobsPath);
112ef919 408
802e75a7
FD
409def failedRuns = []
410def abortedRuns = []
411def isFailed = false
412def isAborted = false
413def ongoingJobs = 0;
414
415currentJobs.each { jobName, jobInfo ->
416 // If the job ran in the past, we check if the IDs changed since.
4e9d9241 417 // Fetch past results only if the job is not of type canary.
c17a93f3 418 if (!jobName.contains('_kcanary_lcanary') && pastJobs.containsKey(jobName) &&
7aab161e 419 build.getBuildVariables().get('FORCE_JOB_RUN') == 'false') {
802e75a7 420 pastJob = pastJobs[jobName];
7cd96a8d
FD
421
422 // If the code has not changed report previous status.
802e75a7
FD
423 if (pastJob['config'] == jobInfo['config']) {
424 // if the config has not changed, we keep it.
425 // if it's failed, we don't launch a new job and keep it failed.
426 jobInfo['status'] = pastJob['status'];
75ecf045
JR
427 if (pastJob['status'] == 'FAILED' &&
428 build.getBuildVariables().get('FORCE_FAILED_JOB_RUN') == 'false') {
802e75a7
FD
429 println("${jobName} as not changed since the last failed run. Don't run it again.");
430 // Marked the umbrella job for failure but still run the jobs that since the
431 // last run.
432 isFailed = true;
433 return;
434 } else if (pastJob['status'] == 'ABORTED') {
435 println("${jobName} as not changed since last aborted run. Run it again.");
436 } else if (pastJob['status'] == 'SUCCEEDED') {
437 println("${jobName} as not changed since the last successful run. Don't run it again.");
438 return;
439 }
440 }
112ef919 441 }
802e75a7
FD
442
443 jobInfo['status'] = 'PENDING';
444 jobInfo['build'] = LaunchJob(jobName, jobInfo);
eedda979
KS
445 if (jobInfo['build'] != null) {
446 ongoingJobs += 1;
447 }
448}
449
450// Some jobs may have a null build immediately if LaunchJob
451// failed for some reason, those jobs can immediately be removed.
452def jobKeys = currentJobs.collect { jobName, jobInfo ->
453 return jobName;
454}
455jobKeys.each { k ->
456 if (currentJobs.get(k)['build'] == null) {
457 println(String.format("Removing job '%s' since build is null", k));
458 currentJobs.remove(k);
459 }
112ef919
FD
460}
461
802e75a7
FD
462while (ongoingJobs > 0) {
463 currentJobs.each { jobName, jobInfo ->
464
465 if (jobInfo['status'] != 'PENDING') {
466 return;
467 }
468
469 jobBuild = jobInfo['build']
5a754cf7
FD
470
471 // The isCancelled() method checks if the run was cancelled before
472 // execution. We consider such run as being aborted.
eedda979 473 if (jobBuild.isCancelled()) {
5a754cf7 474 println("${jobName} was cancelled before launch.")
5a754cf7 475 isAborted = true;
802e75a7
FD
476 abortedRuns.add(jobName);
477 ongoingJobs -= 1;
478 jobInfo['status'] = 'ABORTED'
479 // Invalidate the build field, as it's not serializable and we don't need
480 // it anymore.
481 jobInfo['build'] = null;
482 } else if (jobBuild.isDone()) {
483
484 jobExitStatus = jobBuild.get();
5a754cf7 485
802e75a7
FD
486 // Invalidate the build field, as it's not serializable and we don't need
487 // it anymore.
488 jobInfo['build'] = null;
489 println("${jobExitStatus.fullDisplayName} completed with status ${jobExitStatus.result}.");
5a754cf7
FD
490
491 // If the job didn't succeed, add its name to the right list so it can
492 // be printed at the end of the execution.
802e75a7
FD
493 ongoingJobs -= 1;
494 switch (jobExitStatus.result) {
5a754cf7
FD
495 case Result.ABORTED:
496 isAborted = true;
497 abortedRuns.add(jobName);
802e75a7 498 jobInfo['status'] = 'ABORTED'
5a754cf7
FD
499 break;
500 case Result.FAILURE:
501 isFailed = true;
502 failedRuns.add(jobName);
802e75a7 503 jobInfo['status'] = 'FAILED'
5a754cf7
FD
504 break;
505 case Result.SUCCESS:
802e75a7
FD
506 jobInfo['status'] = 'SUCCEEDED'
507 break;
5a754cf7
FD
508 default:
509 break;
510 }
5a754cf7
FD
511 }
512 }
513
514 // Sleep before the next iteration.
515 try {
b2c3f97c 516 Thread.sleep(30000)
5a754cf7
FD
517 } catch(e) {
518 if (e in InterruptedException) {
519 build.setResult(hudson.model.Result.ABORTED)
520 throw new InterruptedException()
521 } else {
522 throw(e)
523 }
524 }
525}
526
802e75a7
FD
527//All jobs are done running. Save their exit status to disk.
528SaveCurrentJobsToWorkspace(currentJobs, pastJobsPath);
529
5a754cf7
FD
530// Get log of failed runs.
531if (failedRuns.size() > 0) {
532 println("Failed job(s):");
533 for (failedRun in failedRuns) {
534 println("\t" + failedRun)
535 }
536}
537
538// Get log of aborted runs.
539if (abortedRuns.size() > 0) {
540 println("Cancelled job(s):");
541 for (cancelledRun in abortedRuns) {
542 println("\t" + cancelledRun)
543 }
544}
545
546// Mark this build as Failed if atleast one child build has failed and mark as
547// aborted if there was no failure but atleast one job aborted.
548if (isFailed) {
549 build.setResult(hudson.model.Result.FAILURE)
550} else if (isAborted) {
551 build.setResult(hudson.model.Result.ABORTED)
552}
This page took 0.075115 seconds and 4 git commands to generate.