|
| 1 | +/** |
| 2 | + * teuthology-nightly-cadence: expands daily/weekly cadence into SUITE_RUNS_JSON and invokes teuthology-runner. |
| 3 | + * Cadence definitions live here (partition/subset). |
| 4 | + */ |
| 5 | +import java.util.Calendar |
| 6 | +import java.util.TimeZone |
| 7 | + |
| 8 | +pipeline { |
| 9 | + agent { label 'built-in' } |
| 10 | + options { |
| 11 | + timestamps() |
| 12 | + timeout(time: 8, unit: 'HOURS') |
| 13 | + buildDiscarder(logRotator(numToKeepStr: '25')) |
| 14 | + } |
| 15 | + parameters { |
| 16 | + string(name: 'CEPH_BUILD_BRANCH', defaultValue: 'main', description: 'ceph-build branch for teuthology-runner SCM.') |
| 17 | + string(name: 'TEUTHOLOGY_RUNNER_JOB_NAME', defaultValue: 'teuthology-runner', description: 'Jenkins job name for the runner pipeline.') |
| 18 | + string(name: 'AGENT_LABEL', defaultValue: 'teuthology', description: 'Agent label for teuthology-runner.') |
| 19 | + choice(name: 'CADENCE', choices: ['daily', 'weekly'], description: 'daily ≈ nop+smoke; weekly ≈ ceph.git teuthology-cronjobs style.') |
| 20 | + choice(name: 'CEPH_BRANCH', choices: ['main', 'tentacle', 'squid', 'reef'], description: 'Ceph branch.') |
| 21 | + string(name: 'CEPH_REPO', defaultValue: 'https://github.com/ceph/ceph.git', description: 'Ceph git URL.') |
| 22 | + string(name: 'CEPH_SHA1', defaultValue: '', description: 'Optional SHA1. Empty = resolve from Shaman for SHAMAN_WAIT_PLATFORMS, then pass explicitly to teuthology-runner.') |
| 23 | + booleanParam(name: 'USE_WORKSPACE_TEUTHOLOGY', defaultValue: true, description: 'Clone teuthology in runner workspace.') |
| 24 | + string(name: 'TEUTHOLOGY_REPO_URL', defaultValue: 'https://github.com/ceph/teuthology.git', description: 'Teuthology repo.') |
| 25 | + string(name: 'TEUTHOLOGY_BRANCH', defaultValue: 'main', description: 'Teuthology branch.') |
| 26 | + string(name: 'TEUTHOLOGY_SCRIPT_DIR', defaultValue: '', description: 'When USE_WORKSPACE_TEUTHOLOGY false.') |
| 27 | + string(name: 'TEUTHOLOGY_VIRTUALENV_PATH', defaultValue: '', description: 'When USE_WORKSPACE_TEUTHOLOGY false.') |
| 28 | + string(name: 'SUITE_REPO', defaultValue: 'https://github.com/ceph/ceph.git', description: 'teuthology-suite --suite-repo.') |
| 29 | + string(name: 'SUITE_MACHINE_TYPE', defaultValue: 'smithi', description: 'teuthology-suite --machine-type.') |
| 30 | + string(name: 'TEUTH_CONFIG_OVERRIDE_YAML', defaultValue: '', description: 'Optional teuthology-suite argv.') |
| 31 | + string(name: 'SHAMAN_WAIT_TIMEOUT', defaultValue: '7200', description: 'Shaman wait timeout (seconds).') |
| 32 | + string(name: 'SHAMAN_WAIT_INTERVAL', defaultValue: '120', description: 'Shaman poll interval.') |
| 33 | + string(name: 'SHAMAN_WAIT_PLATFORMS', defaultValue: 'rocky-10-default,ubuntu-jammy-default,centos-9-default', description: 'Shaman platforms.') |
| 34 | + booleanParam(name: 'SKIP_SHAMAN_WAIT', defaultValue: false, description: 'Skip Shaman wait.') |
| 35 | + booleanParam(name: 'WAIT_FOR_RUNS', defaultValue: false, description: 'teuthology-wait in runner.') |
| 36 | + string(name: 'SUITE_WAIT_SLEEP', defaultValue: '15', description: 'Sleep before teuthology-wait.') |
| 37 | + booleanParam(name: 'RUN_AGGREGATE', defaultValue: false, description: 'Aggregate in runner.') |
| 38 | + string(name: 'PADDLES_URL', defaultValue: '', description: 'Optional Paddles override.') |
| 39 | + string(name: 'PULPITO_BASE', defaultValue: 'https://pulpito.ceph.com', description: 'Pulpito base URL for teuthology-runner.') |
| 40 | + } |
| 41 | + stages { |
| 42 | + stage('teuthology-runner') { |
| 43 | + steps { |
| 44 | + script { |
| 45 | + def runnerJob = (params.TEUTHOLOGY_RUNNER_JOB_NAME ?: 'teuthology-runner').trim() |
| 46 | + if (!runnerJob) { |
| 47 | + runnerJob = 'teuthology-runner' |
| 48 | + } |
| 49 | + def resolvedSha = params.CEPH_SHA1?.trim() |
| 50 | + if (!resolvedSha) { |
| 51 | + def branch = params.CEPH_BRANCH?.trim() ?: '' |
| 52 | + if (!(branch ==~ '^[a-zA-Z0-9/._-]+$')) { |
| 53 | + error("CEPH_BRANCH contains unsupported characters: ${branch}") |
| 54 | + } |
| 55 | + def timeout = params.SHAMAN_WAIT_TIMEOUT?.trim() ?: '7200' |
| 56 | + def interval = params.SHAMAN_WAIT_INTERVAL?.trim() ?: '120' |
| 57 | + if (!(timeout ==~ '^[0-9]+$') || timeout == '0') { |
| 58 | + error("SHAMAN_WAIT_TIMEOUT must be a positive integer: ${timeout}") |
| 59 | + } |
| 60 | + if (!(interval ==~ '^[0-9]+$') || interval == '0') { |
| 61 | + error("SHAMAN_WAIT_INTERVAL must be a positive integer: ${interval}") |
| 62 | + } |
| 63 | + def platforms = params.SHAMAN_WAIT_PLATFORMS?.trim() ?: 'rocky-10-default,ubuntu-jammy-default,centos-9-default' |
| 64 | + def shamanScript = "${env.WORKSPACE}/teuthology-runner/scripts/wait_for_shaman_sha1.py" |
| 65 | + if (!fileExists(shamanScript)) { |
| 66 | + error("Missing ${shamanScript}") |
| 67 | + } |
| 68 | + resolvedSha = sh( |
| 69 | + script: "python3 \"${shamanScript}\" --branch \"${branch}\" --timeout \"${timeout}\" --interval \"${interval}\" --platform \"${platforms}\" --use-available-sha", |
| 70 | + returnStdout: true, |
| 71 | + ).trim() |
| 72 | + echo "Resolved CEPH_SHA1 from Shaman for branch=${branch} platforms=${platforms}: ${resolvedSha}" |
| 73 | + } else { |
| 74 | + echo "Using explicit CEPH_SHA1 from parameters: ${resolvedSha}" |
| 75 | + } |
| 76 | + if (!resolvedSha || !(resolvedSha ==~ /^[a-fA-F0-9]{7,40}$/)) { |
| 77 | + error("Resolved CEPH_SHA1 is invalid: ${resolvedSha}") |
| 78 | + } |
| 79 | + def rows = expandCadenceToRows(params.CADENCE, params.CEPH_BRANCH) |
| 80 | + if (!rows) { |
| 81 | + error("No suites for CADENCE=${params.CADENCE} CEPH_BRANCH=${params.CEPH_BRANCH}") |
| 82 | + } |
| 83 | + def json = suiteRunsRowsToJson(rows) |
| 84 | + def tp = [] |
| 85 | + tp << string(name: 'AGENT_LABEL', value: params.AGENT_LABEL.trim()) |
| 86 | + tp << string(name: 'CEPH_BUILD_BRANCH', value: params.CEPH_BUILD_BRANCH.trim()) |
| 87 | + tp << string(name: 'CEPH_BRANCH', value: params.CEPH_BRANCH) |
| 88 | + tp << string(name: 'CEPH_REPO', value: params.CEPH_REPO.trim()) |
| 89 | + tp << string(name: 'CEPH_SHA1', value: resolvedSha) |
| 90 | + tp << booleanParam(name: 'USE_WORKSPACE_TEUTHOLOGY', value: params.USE_WORKSPACE_TEUTHOLOGY) |
| 91 | + tp << string(name: 'TEUTHOLOGY_REPO_URL', value: params.TEUTHOLOGY_REPO_URL.trim()) |
| 92 | + tp << string(name: 'TEUTHOLOGY_BRANCH', value: params.TEUTHOLOGY_BRANCH.trim()) |
| 93 | + if (!params.USE_WORKSPACE_TEUTHOLOGY) { |
| 94 | + tp << string(name: 'TEUTHOLOGY_SCRIPT_DIR', value: params.TEUTHOLOGY_SCRIPT_DIR?.trim() ?: '') |
| 95 | + tp << string(name: 'TEUTHOLOGY_VIRTUALENV_PATH', value: params.TEUTHOLOGY_VIRTUALENV_PATH?.trim() ?: '') |
| 96 | + } |
| 97 | + if (params.TEUTH_CONFIG_OVERRIDE_YAML?.trim()) { |
| 98 | + tp << string(name: 'TEUTH_CONFIG_OVERRIDE_YAML', value: params.TEUTH_CONFIG_OVERRIDE_YAML.trim()) |
| 99 | + } |
| 100 | + tp << string(name: 'SUITE_REPO', value: params.SUITE_REPO.trim()) |
| 101 | + tp << string(name: 'SUITE_MACHINE_TYPE', value: params.SUITE_MACHINE_TYPE.trim()) |
| 102 | + if (params.PADDLES_URL?.trim()) { |
| 103 | + tp << string(name: 'PADDLES_URL', value: params.PADDLES_URL.trim()) |
| 104 | + } |
| 105 | + tp << string(name: 'SHAMAN_WAIT_TIMEOUT', value: params.SHAMAN_WAIT_TIMEOUT.trim()) |
| 106 | + tp << string(name: 'SHAMAN_WAIT_INTERVAL', value: params.SHAMAN_WAIT_INTERVAL.trim()) |
| 107 | + tp << string(name: 'SHAMAN_WAIT_PLATFORMS', value: params.SHAMAN_WAIT_PLATFORMS.trim()) |
| 108 | + tp << string(name: 'PULPITO_BASE', value: (params.PULPITO_BASE ?: 'https://pulpito.ceph.com').trim()) |
| 109 | + tp << booleanParam(name: 'SKIP_SHAMAN_WAIT', value: params.SKIP_SHAMAN_WAIT) |
| 110 | + tp << booleanParam(name: 'WAIT_FOR_RUNS', value: params.WAIT_FOR_RUNS) |
| 111 | + tp << string(name: 'SUITE_WAIT_SLEEP', value: params.SUITE_WAIT_SLEEP.trim()) |
| 112 | + tp << booleanParam(name: 'RUN_AGGREGATE', value: params.RUN_AGGREGATE) |
| 113 | + tp << text(name: 'SUITE_RUNS_JSON', value: json) |
| 114 | + build job: runnerJob, wait: true, propagate: true, parameters: tp |
| 115 | + } |
| 116 | + } |
| 117 | + } |
| 118 | + } |
| 119 | +} |
| 120 | + |
| 121 | +List expandCadenceToRows(String cadence, String branch) { |
| 122 | + def steps = cadenceSteps(cadence, branch.toLowerCase()) |
| 123 | + if (!steps) { |
| 124 | + return [] |
| 125 | + } |
| 126 | + def utcCal = Calendar.getInstance(TimeZone.getTimeZone('UTC')) |
| 127 | + def doy = utcCal.get(Calendar.DAY_OF_YEAR) |
| 128 | + def rows = [] |
| 129 | + for (def st in steps) { |
| 130 | + def part = st.partitions as int |
| 131 | + def idx = doy % part |
| 132 | + def subset = "${idx}/${part}" |
| 133 | + def lim = st.limit != null ? st.limit as String : "${part}" |
| 134 | + def thr = st.threshold != null ? st.threshold as String : "${part}" |
| 135 | + def pr = st.priority ?: '' |
| 136 | + def m = [ |
| 137 | + suite: st.suite, |
| 138 | + limit: lim, |
| 139 | + threshold: thr, |
| 140 | + subset: subset, |
| 141 | + priority: pr, |
| 142 | + forcePriority: st.forcePriority ? true : false, |
| 143 | + ] |
| 144 | + if (st.flavor) { |
| 145 | + m.flavor = st.flavor |
| 146 | + } |
| 147 | + if (st.kernel) { |
| 148 | + m.kernel = st.kernel |
| 149 | + } |
| 150 | + if (st.filter) { |
| 151 | + m.filter = st.filter |
| 152 | + } |
| 153 | + rows << m |
| 154 | + } |
| 155 | + return rows |
| 156 | +} |
| 157 | + |
| 158 | +def cadenceSteps(String cadence, String branch) { |
| 159 | + def b = branch.toLowerCase() |
| 160 | + if (cadence?.toLowerCase() == 'daily') { |
| 161 | + return [ |
| 162 | + [suite: 'teuthology/nop', partitions: 1, priority: '1', forcePriority: true], |
| 163 | + [suite: 'smoke', partitions: 1, priority: '100', forcePriority: true], |
| 164 | + ] |
| 165 | + } |
| 166 | + if (cadence?.toLowerCase() != 'weekly') { |
| 167 | + return [] |
| 168 | + } |
| 169 | + switch (b) { |
| 170 | + case 'main': |
| 171 | + return [ |
| 172 | + [suite: 'rados', partitions: 100000, priority: '101', forcePriority: true], |
| 173 | + [suite: 'rados', partitions: 100000, priority: '101', forcePriority: true, flavor: 'debug'], |
| 174 | + [suite: 'orch', partitions: 64, priority: '950', forcePriority: false], |
| 175 | + [suite: 'rbd', partitions: 128, priority: '950', forcePriority: false], |
| 176 | + [suite: 'fs', partitions: 512, priority: '700', forcePriority: false], |
| 177 | + [suite: 'powercycle', partitions: 4, priority: '950', forcePriority: false], |
| 178 | + [suite: 'rgw', partitions: 30000, limit: '1', threshold: '1', priority: '150', forcePriority: false], |
| 179 | + [suite: 'krbd', partitions: 4, priority: '950', forcePriority: false, kernel: 'testing'], |
| 180 | + [suite: 'crimson-rados', partitions: 1, priority: '101', forcePriority: true, flavor: 'debug'], |
| 181 | + [suite: 'crimson-rados', partitions: 1, priority: '101', forcePriority: true], |
| 182 | + ] |
| 183 | + case 'tentacle': |
| 184 | + return [ |
| 185 | + [suite: 'rados', partitions: 100000, priority: '831', forcePriority: false], |
| 186 | + [suite: 'orch', partitions: 64, priority: '830', forcePriority: false], |
| 187 | + [suite: 'rbd', partitions: 128, priority: '830', forcePriority: false], |
| 188 | + [suite: 'fs', partitions: 512, priority: '830', forcePriority: false], |
| 189 | + [suite: 'powercycle', partitions: 4, priority: '830', forcePriority: false], |
| 190 | + [suite: 'rgw', partitions: 30000, limit: '1', threshold: '1', priority: '830', forcePriority: false], |
| 191 | + [suite: 'krbd', partitions: 4, priority: '830', forcePriority: false, kernel: 'testing'], |
| 192 | + ] |
| 193 | + case 'squid': |
| 194 | + return [ |
| 195 | + [suite: 'rados', partitions: 100000, priority: '921', forcePriority: false], |
| 196 | + [suite: 'orch', partitions: 64, priority: '920', forcePriority: false], |
| 197 | + [suite: 'rbd', partitions: 128, priority: '920', forcePriority: false], |
| 198 | + [suite: 'fs', partitions: 512, priority: '920', forcePriority: false], |
| 199 | + [suite: 'powercycle', partitions: 4, priority: '920', forcePriority: false], |
| 200 | + [suite: 'rgw', partitions: 30000, limit: '1', threshold: '1', priority: '920', forcePriority: false], |
| 201 | + [suite: 'krbd', partitions: 4, priority: '920', forcePriority: false, kernel: 'testing'], |
| 202 | + ] |
| 203 | + case 'reef': |
| 204 | + return [ |
| 205 | + [suite: 'rados', partitions: 100000, priority: '920', forcePriority: false], |
| 206 | + [suite: 'orch', partitions: 64, priority: '920', forcePriority: false], |
| 207 | + [suite: 'rbd', partitions: 128, priority: '920', forcePriority: false], |
| 208 | + [suite: 'fs', partitions: 512, priority: '920', forcePriority: false], |
| 209 | + [suite: 'powercycle', partitions: 4, priority: '920', forcePriority: false], |
| 210 | + [suite: 'rgw', partitions: 30000, limit: '1', threshold: '1', priority: '920', forcePriority: false], |
| 211 | + [suite: 'krbd', partitions: 4, priority: '920', forcePriority: false, kernel: 'testing'], |
| 212 | + ] |
| 213 | + default: |
| 214 | + return [] |
| 215 | + } |
| 216 | +} |
| 217 | + |
| 218 | +String suiteRunsRowsToJson(List rows) { |
| 219 | + def sb = new StringBuilder() |
| 220 | + sb.append('[') |
| 221 | + def first = true |
| 222 | + for (def row in rows) { |
| 223 | + if (!first) { |
| 224 | + sb.append(',') |
| 225 | + } |
| 226 | + first = false |
| 227 | + sb.append('{') |
| 228 | + def f2 = true |
| 229 | + row.each { k, v -> |
| 230 | + if (!f2) { |
| 231 | + sb.append(',') |
| 232 | + } |
| 233 | + f2 = false |
| 234 | + sb.append('"').append(k).append('":') |
| 235 | + if (v instanceof Boolean) { |
| 236 | + sb.append(v ? 'true' : 'false') |
| 237 | + } else if (v instanceof Number) { |
| 238 | + sb.append(v.toString()) |
| 239 | + } else { |
| 240 | + sb.append('"').append(jsonStringEscape(v != null ? v.toString() : '')).append('"') |
| 241 | + } |
| 242 | + } |
| 243 | + sb.append('}') |
| 244 | + } |
| 245 | + sb.append(']') |
| 246 | + return sb.toString() |
| 247 | +} |
| 248 | + |
| 249 | +String jsonStringEscape(String s) { |
| 250 | + if (s == null) { |
| 251 | + return '' |
| 252 | + } |
| 253 | + return s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t') |
| 254 | +} |
0 commit comments