blob: 177f546d686acbbeea2c2c10e1408b5bc77b5b7f [file] [log] [blame]
Jakub Josef79ecec32017-02-17 14:36:28 +01001package com.mirantis.mk
Jakub Josef6d8082b2017-08-09 12:40:50 +02002
Jakub Josefb41c8d52017-03-24 13:52:24 +01003import static groovy.json.JsonOutput.prettyPrint
4import static groovy.json.JsonOutput.toJson
Jakub Josef6d8082b2017-08-09 12:40:50 +02005
Jakub Josefbceaa322017-06-13 18:28:27 +02006import com.cloudbees.groovy.cps.NonCPS
Jakub Josefb7ab8472017-04-05 14:56:53 +02007import groovy.json.JsonSlurperClassic
azvyagintsev6bda9422018-08-20 11:57:05 +03008
Jakub Josef79ecec32017-02-17 14:36:28 +01009/**
10 *
11 * Common functions
12 *
13 */
14
15/**
16 * Generate current timestamp
17 *
azvyagintsev6bda9422018-08-20 11:57:05 +030018 * @param format Defaults to yyyyMMddHHmmss
Jakub Josef79ecec32017-02-17 14:36:28 +010019 */
azvyagintsev6bda9422018-08-20 11:57:05 +030020def getDatetime(format = "yyyyMMddHHmmss") {
Jakub Josef79ecec32017-02-17 14:36:28 +010021 def now = new Date();
22 return now.format(format, TimeZone.getTimeZone('UTC'));
23}
24
25/**
Jakub Josef79ecec32017-02-17 14:36:28 +010026 * Return workspace.
27 * Currently implemented by calling pwd so it won't return relevant result in
28 * dir context
29 */
azvyagintsev6bda9422018-08-20 11:57:05 +030030def getWorkspace(includeBuildNum = false) {
Jakub Josef79ecec32017-02-17 14:36:28 +010031 def workspace = sh script: 'pwd', returnStdout: true
32 workspace = workspace.trim()
azvyagintsev6bda9422018-08-20 11:57:05 +030033 if (includeBuildNum) {
34 if (!workspace.endsWith("/")) {
35 workspace += "/"
36 }
37 workspace += env.BUILD_NUMBER
Jakub Josefa661b8c2018-01-17 14:51:25 +010038 }
Jakub Josef79ecec32017-02-17 14:36:28 +010039 return workspace
40}
41
42/**
Filip Pytloun81c864d2017-03-21 15:19:30 +010043 * Get UID of jenkins user.
44 * Must be run from context of node
45 */
46def getJenkinsUid() {
azvyagintsev6bda9422018-08-20 11:57:05 +030047 return sh(
Filip Pytloun81c864d2017-03-21 15:19:30 +010048 script: 'id -u',
49 returnStdout: true
50 ).trim()
51}
52
53/**
54 * Get GID of jenkins user.
55 * Must be run from context of node
56 */
57def getJenkinsGid() {
azvyagintsev6bda9422018-08-20 11:57:05 +030058 return sh(
Filip Pytloun81c864d2017-03-21 15:19:30 +010059 script: 'id -g',
60 returnStdout: true
61 ).trim()
62}
63
64/**
Jakub Josefc8074db2018-01-30 13:33:20 +010065 * Returns Jenkins user uid and gid in one list (in that order)
66 * Must be run from context of node
67 */
azvyagintsev6bda9422018-08-20 11:57:05 +030068def getJenkinsUserIds() {
Jakub Josefc8074db2018-01-30 13:33:20 +010069 return sh(script: "id -u && id -g", returnStdout: true).tokenize("\n")
70}
71
72/**
Alexander Evseevbc1fea42017-12-13 10:03:03 +010073 *
74 * Find credentials by ID
75 *
azvyagintsev6bda9422018-08-20 11:57:05 +030076 * @param credsId Credentials ID
77 * @param credsType Credentials type (optional)
Alexander Evseevbc1fea42017-12-13 10:03:03 +010078 *
79 */
80def getCredentialsById(String credsId, String credsType = 'any') {
81 def credClasses = [ // ordered by class name
azvyagintsev6bda9422018-08-20 11:57:05 +030082 sshKey : com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey.class,
83 cert : com.cloudbees.plugins.credentials.common.CertificateCredentials.class,
84 password : com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials.class,
85 any : com.cloudbees.plugins.credentials.impl.BaseStandardCredentials.class,
86 dockerCert: org.jenkinsci.plugins.docker.commons.credentials.DockerServerCredentials.class,
87 file : org.jenkinsci.plugins.plaincredentials.FileCredentials.class,
88 string : org.jenkinsci.plugins.plaincredentials.StringCredentials.class,
Alexander Evseevbc1fea42017-12-13 10:03:03 +010089 ]
90 return com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
91 credClasses[credsType],
92 jenkins.model.Jenkins.instance
azvyagintsev6bda9422018-08-20 11:57:05 +030093 ).findAll { cred -> cred.id == credsId }[0]
Alexander Evseevbc1fea42017-12-13 10:03:03 +010094}
95
96/**
Jakub Josef79ecec32017-02-17 14:36:28 +010097 * Get credentials from store
98 *
azvyagintsev6bda9422018-08-20 11:57:05 +030099 * @param id Credentials name
Jakub Josef79ecec32017-02-17 14:36:28 +0100100 */
Jakub Josef3d9d9ab2017-03-14 15:09:03 +0100101def getCredentials(id, cred_type = "username_password") {
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100102 warningMsg('You are using obsolete function. Please switch to use `getCredentialsById()`')
Jakub Josef79ecec32017-02-17 14:36:28 +0100103
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100104 type_map = [
105 username_password: 'password',
azvyagintsev6bda9422018-08-20 11:57:05 +0300106 key : 'sshKey',
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100107 ]
Jakub Josef79ecec32017-02-17 14:36:28 +0100108
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100109 return getCredentialsById(id, type_map[cred_type])
Jakub Josef79ecec32017-02-17 14:36:28 +0100110}
111
112/**
113 * Abort build, wait for some time and ensure we will terminate
114 */
115def abortBuild() {
116 currentBuild.build().doStop()
117 sleep(180)
118 // just to be sure we will terminate
119 throw new InterruptedException()
120}
121
122/**
Jakub Josefbceaa322017-06-13 18:28:27 +0200123 * Print pretty-printed string representation of given item
124 * @param item item to be pretty-printed (list, map, whatever)
125 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300126def prettyPrint(item) {
Jakub Josefbceaa322017-06-13 18:28:27 +0200127 println prettify(item)
128}
129
130/**
Jakub Josefb41c8d52017-03-24 13:52:24 +0100131 * Return pretty-printed string representation of given item
132 * @param item item to be pretty-printed (list, map, whatever)
133 * @return pretty-printed string
134 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300135def prettify(item) {
Jakub Josefbceaa322017-06-13 18:28:27 +0200136 return groovy.json.JsonOutput.prettyPrint(toJson(item)).replace('\\n', System.getProperty('line.separator'))
Jakub Josefb41c8d52017-03-24 13:52:24 +0100137}
138
139/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100140 * Print informational message
141 *
142 * @param msg
143 * @param color Colorful output or not
144 */
145def infoMsg(msg, color = true) {
146 printMsg(msg, "cyan")
147}
148
149/**
150 * Print error message
151 *
152 * @param msg
153 * @param color Colorful output or not
154 */
155def errorMsg(msg, color = true) {
156 printMsg(msg, "red")
157}
158
159/**
160 * Print success message
161 *
162 * @param msg
163 * @param color Colorful output or not
164 */
165def successMsg(msg, color = true) {
166 printMsg(msg, "green")
167}
168
169/**
170 * Print warning message
171 *
172 * @param msg
173 * @param color Colorful output or not
174 */
175def warningMsg(msg, color = true) {
Jakub Josef0e7bd632017-03-16 16:25:05 +0100176 printMsg(msg, "yellow")
Jakub Josef79ecec32017-02-17 14:36:28 +0100177}
178
179/**
Jakub Josef952ae0b2017-03-14 19:04:21 +0100180 * Print debug message, this message will show only if DEBUG global variable is present
181 * @param msg
182 * @param color Colorful output or not
183 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300184def debugMsg(msg, color = true) {
Jakub Josef9a836ac2017-04-24 12:26:02 +0200185 // if debug property exists on env, debug is enabled
azvyagintsev6bda9422018-08-20 11:57:05 +0300186 if (env.getEnvironment().containsKey('DEBUG') && env['DEBUG'] == "true") {
Jakub Josef74b34692017-03-15 12:10:57 +0100187 printMsg("[DEBUG] ${msg}", "red")
Jakub Josef952ae0b2017-03-14 19:04:21 +0100188 }
189}
190
azvyagintsevf6e77912018-09-07 15:41:09 +0300191def getColorizedString(msg, color) {
192 def colorMap = [
193 'red' : '\u001B[31m',
194 'black' : '\u001B[30m',
195 'green' : '\u001B[32m',
196 'yellow': '\u001B[33m',
197 'blue' : '\u001B[34m',
198 'purple': '\u001B[35m',
199 'cyan' : '\u001B[36m',
200 'white' : '\u001B[37m',
201 'reset' : '\u001B[0m'
202 ]
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300203
azvyagintsevf6e77912018-09-07 15:41:09 +0300204 return "${colorMap[color]}${msg}${colorMap.reset}"
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300205}
206
Jakub Josef952ae0b2017-03-14 19:04:21 +0100207/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100208 * Print message
209 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300210 * @param msg Message to be printed
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300211 * @param color Color to use for output
Jakub Josef79ecec32017-02-17 14:36:28 +0100212 */
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300213def printMsg(msg, color) {
214 print getColorizedString(msg, color)
Jakub Josef79ecec32017-02-17 14:36:28 +0100215}
216
217/**
218 * Traverse directory structure and return list of files
219 *
220 * @param path Path to search
221 * @param type Type of files to search (groovy.io.FileType.FILES)
222 */
223@NonCPS
azvyagintsev6bda9422018-08-20 11:57:05 +0300224def getFiles(path, type = groovy.io.FileType.FILES) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100225 files = []
226 new File(path).eachFile(type) {
227 files[] = it
228 }
229 return files
230}
231
232/**
233 * Helper method to convert map into form of list of [key,value] to avoid
234 * unserializable exceptions
235 *
236 * @param m Map
237 */
238@NonCPS
239def entries(m) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300240 m.collect { k, v -> [k, v] }
Jakub Josef79ecec32017-02-17 14:36:28 +0100241}
242
243/**
244 * Opposite of build-in parallel, run map of steps in serial
245 *
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200246 * @param steps Map of String<name>: CPSClosure2<step> (or list of closures)
Jakub Josef79ecec32017-02-17 14:36:28 +0100247 */
248def serial(steps) {
249 stepsArray = entries(steps)
azvyagintsev6bda9422018-08-20 11:57:05 +0300250 for (i = 0; i < stepsArray.size; i++) {
Jakub Josefd31de302017-05-15 13:59:18 +0200251 def step = stepsArray[i]
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200252 def dummySteps = [:]
253 def stepKey
azvyagintsev6bda9422018-08-20 11:57:05 +0300254 if (step[1] instanceof List || step[1] instanceof Map) {
255 for (j = 0; j < step[1].size(); j++) {
256 if (step[1] instanceof List) {
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200257 stepKey = j
azvyagintsev6bda9422018-08-20 11:57:05 +0300258 } else if (step[1] instanceof Map) {
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200259 stepKey = step[1].keySet()[j]
260 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300261 dummySteps.put("step-${step[0]}-${stepKey}", step[1][stepKey])
Jakub Josefd31de302017-05-15 13:59:18 +0200262 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300263 } else {
Jakub Josefd31de302017-05-15 13:59:18 +0200264 dummySteps.put(step[0], step[1])
265 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100266 parallel dummySteps
267 }
268}
269
270/**
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200271 * Partition given list to list of small lists
272 * @param inputList input list
273 * @param partitionSize (partition size, optional, default 5)
274 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300275def partitionList(inputList, partitionSize = 5) {
276 List<List<String>> partitions = new ArrayList<>();
277 for (int i = 0; i < inputList.size(); i += partitionSize) {
278 partitions.add(new ArrayList<String>(inputList.subList(i, Math.min(i + partitionSize, inputList.size()))));
279 }
280 return partitions
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200281}
282
283/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100284 * Get password credentials from store
285 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300286 * @param id Credentials name
Jakub Josef79ecec32017-02-17 14:36:28 +0100287 */
288def getPasswordCredentials(id) {
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100289 return getCredentialsById(id, 'password')
Jakub Josef79ecec32017-02-17 14:36:28 +0100290}
291
292/**
293 * Get SSH credentials from store
294 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300295 * @param id Credentials name
Jakub Josef79ecec32017-02-17 14:36:28 +0100296 */
297def getSshCredentials(id) {
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100298 return getCredentialsById(id, 'sshKey')
Jakub Josef79ecec32017-02-17 14:36:28 +0100299}
Jakub Josef79ecec32017-02-17 14:36:28 +0100300
301/**
302 * Tests Jenkins instance for existence of plugin with given name
303 * @param pluginName plugin short name to test
304 * @return boolean result
305 */
306@NonCPS
azvyagintsev6bda9422018-08-20 11:57:05 +0300307def jenkinsHasPlugin(pluginName) {
308 return Jenkins.instance.pluginManager.plugins.collect { p -> p.shortName }.contains(pluginName)
Jakub Josef79ecec32017-02-17 14:36:28 +0100309}
310
311@NonCPS
312def _needNotification(notificatedTypes, buildStatus, jobName) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300313 if (notificatedTypes && notificatedTypes.contains("onchange")) {
314 if (jobName) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100315 def job = Jenkins.instance.getItem(jobName)
316 def numbuilds = job.builds.size()
azvyagintsev6bda9422018-08-20 11:57:05 +0300317 if (numbuilds > 0) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100318 //actual build is first for some reasons, so last finished build is second
319 def lastBuild = job.builds[1]
azvyagintsev6bda9422018-08-20 11:57:05 +0300320 if (lastBuild) {
321 if (lastBuild.result.toString().toLowerCase().equals(buildStatus)) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100322 println("Build status didn't changed since last build, not sending notifications")
323 return false;
324 }
325 }
326 }
327 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300328 } else if (!notificatedTypes.contains(buildStatus)) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100329 return false;
330 }
331 return true;
332}
333
334/**
335 * Send notification to all enabled notifications services
336 * @param buildStatus message type (success, warning, error), null means SUCCESSFUL
337 * @param msgText message text
338 * @param enabledNotifications list of enabled notification types, types: slack, hipchat, email, default empty
339 * @param notificatedTypes types of notifications will be sent, default onchange - notificate if current build result not equal last result;
340 * otherwise use - ["success","unstable","failed"]
341 * @param jobName optional job name param, if empty env.JOB_NAME will be used
Jakub Josefd0571152017-07-17 14:11:39 +0200342 * @param buildNumber build number param, if empty env.BUILD_NUM will be used
343 * @param buildUrl build url param, if empty env.BUILD_URL will be used
Jakub Josef79ecec32017-02-17 14:36:28 +0100344 * @param mailFrom mail FROM param, if empty "jenkins" will be used, it's mandatory for sending email notifications
Jakub Josefd0571152017-07-17 14:11:39 +0200345 * @param mailTo mail TO param, it's mandatory for sending email notifications, this option enable mail notification
Jakub Josef79ecec32017-02-17 14:36:28 +0100346 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300347def sendNotification(buildStatus, msgText = "", enabledNotifications = [], notificatedTypes = ["onchange"], jobName = null, buildNumber = null, buildUrl = null, mailFrom = "jenkins", mailTo = null) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100348 // Default values
349 def colorName = 'blue'
350 def colorCode = '#0000FF'
351 def buildStatusParam = buildStatus != null && buildStatus != "" ? buildStatus : "SUCCESS"
352 def jobNameParam = jobName != null && jobName != "" ? jobName : env.JOB_NAME
353 def buildNumberParam = buildNumber != null && buildNumber != "" ? buildNumber : env.BUILD_NUMBER
354 def buildUrlParam = buildUrl != null && buildUrl != "" ? buildUrl : env.BUILD_URL
355 def subject = "${buildStatusParam}: Job '${jobNameParam} [${buildNumberParam}]'"
356 def summary = "${subject} (${buildUrlParam})"
357
azvyagintsev6bda9422018-08-20 11:57:05 +0300358 if (msgText != null && msgText != "") {
359 summary += "\n${msgText}"
Jakub Josef79ecec32017-02-17 14:36:28 +0100360 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300361 if (buildStatusParam.toLowerCase().equals("success")) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100362 colorCode = "#00FF00"
363 colorName = "green"
azvyagintsev6bda9422018-08-20 11:57:05 +0300364 } else if (buildStatusParam.toLowerCase().equals("unstable")) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100365 colorCode = "#FFFF00"
366 colorName = "yellow"
azvyagintsev6bda9422018-08-20 11:57:05 +0300367 } else if (buildStatusParam.toLowerCase().equals("failure")) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100368 colorCode = "#FF0000"
369 colorName = "red"
370 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300371 if (_needNotification(notificatedTypes, buildStatusParam.toLowerCase(), jobNameParam)) {
372 if (enabledNotifications.contains("slack") && jenkinsHasPlugin("slack")) {
373 try {
Jakub Josef79ecec32017-02-17 14:36:28 +0100374 slackSend color: colorCode, message: summary
azvyagintsev6bda9422018-08-20 11:57:05 +0300375 } catch (Exception e) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100376 println("Calling slack plugin failed")
377 e.printStackTrace()
378 }
379 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300380 if (enabledNotifications.contains("hipchat") && jenkinsHasPlugin("hipchat")) {
381 try {
Jakub Josef79ecec32017-02-17 14:36:28 +0100382 hipchatSend color: colorName.toUpperCase(), message: summary
azvyagintsev6bda9422018-08-20 11:57:05 +0300383 } catch (Exception e) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100384 println("Calling hipchat plugin failed")
385 e.printStackTrace()
386 }
387 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300388 if (enabledNotifications.contains("email") && mailTo != null && mailTo != "" && mailFrom != null && mailFrom != "") {
389 try {
Jakub Josef79ecec32017-02-17 14:36:28 +0100390 mail body: summary, from: mailFrom, subject: subject, to: mailTo
azvyagintsev6bda9422018-08-20 11:57:05 +0300391 } catch (Exception e) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100392 println("Sending mail plugin failed")
393 e.printStackTrace()
394 }
395 }
396 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100397}
chnyda4e5ac792017-03-14 15:24:18 +0100398
399/**
400 * Execute linux command and catch nth element
401 * @param cmd command to execute
402 * @param index index to retrieve
403 * @return index-th element
404 */
405
azvyagintsev6bda9422018-08-20 11:57:05 +0300406def cutOrDie(cmd, index) {
chnyda4e5ac792017-03-14 15:24:18 +0100407 def common = new com.mirantis.mk.Common()
408 def output
409 try {
azvyagintsev6bda9422018-08-20 11:57:05 +0300410 output = sh(script: cmd, returnStdout: true)
411 def result = output.tokenize(" ")[index]
412 return result;
chnyda4e5ac792017-03-14 15:24:18 +0100413 } catch (Exception e) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300414 common.errorMsg("Failed to execute cmd: ${cmd}\n output: ${output}")
chnyda4e5ac792017-03-14 15:24:18 +0100415 }
Filip Pytloun81c864d2017-03-21 15:19:30 +0100416}
Tomáš Kukrál767dd732017-03-23 10:38:59 +0100417
418/**
419 * Check variable contains keyword
420 * @param variable keywork is searched (contains) here
421 * @param keyword string to look for
422 * @return True if variable contains keyword (case insensitive), False if do not contains or any of input isn't a string
423 */
424
425def checkContains(variable, keyword) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300426 if (env.getEnvironment().containsKey(variable)) {
Jakub Josef7a8dea22017-03-23 19:51:32 +0100427 return env[variable] && env[variable].toLowerCase().contains(keyword.toLowerCase())
Tomáš Kukrál767dd732017-03-23 10:38:59 +0100428 } else {
Tomáš Kukrálc76c1e02017-03-23 19:06:59 +0100429 return false
Tomáš Kukrál767dd732017-03-23 10:38:59 +0100430 }
431}
Jakub Josefa877db52017-04-05 14:22:30 +0200432
433/**
434 * Parse JSON string to hashmap
435 * @param jsonString input JSON string
436 * @return created hashmap
437 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300438def parseJSON(jsonString) {
439 def m = [:]
440 def lazyMap = new JsonSlurperClassic().parseText(jsonString)
441 m.putAll(lazyMap)
442 return m
Jakub Josefa877db52017-04-05 14:22:30 +0200443}
Jakub Josefed239cd2017-05-09 15:27:33 +0200444
445/**
446 * Test pipeline input parameter existence and validity (not null and not empty string)
447 * @param paramName input parameter name (usually uppercase)
448 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300449def validInputParam(paramName) {
Jakub Josefed239cd2017-05-09 15:27:33 +0200450 return env.getEnvironment().containsKey(paramName) && env[paramName] != null && env[paramName] != ""
Tomáš Kukráld34fe872017-06-13 10:50:50 +0200451}
452
453/**
454 * Take list of hashmaps and count number of hashmaps with parameter equals eq
455 * @param lm list of hashmaps
456 * @param param define parameter of hashmap to read and compare
457 * @param eq desired value of hashmap parameter
458 * @return count of hashmaps meeting defined condition
459 */
460
461@NonCPS
462def countHashMapEquals(lm, param, eq) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300463 return lm.stream().filter { i -> i[param].equals(eq) }.collect(java.util.stream.Collectors.counting())
Tomáš Kukráld34fe872017-06-13 10:50:50 +0200464}
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200465
466/**
467 * Execute shell command and return stdout, stderr and status
468 *
469 * @param cmd Command to execute
470 * @return map with stdout, stderr, status keys
471 */
472
473def shCmdStatus(cmd) {
474 def res = [:]
475 def stderr = sh(script: 'mktemp', returnStdout: true).trim()
476 def stdout = sh(script: 'mktemp', returnStdout: true).trim()
477
478 try {
azvyagintsev6bda9422018-08-20 11:57:05 +0300479 def status = sh(script: "${cmd} 1>${stdout} 2>${stderr}", returnStatus: true)
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200480 res['stderr'] = sh(script: "cat ${stderr}", returnStdout: true)
481 res['stdout'] = sh(script: "cat ${stdout}", returnStdout: true)
482 res['status'] = status
483 } finally {
484 sh(script: "rm ${stderr}", returnStdout: true)
485 sh(script: "rm ${stdout}", returnStdout: true)
486 }
487
488 return res
489}
Richard Felkl66a242d2018-01-25 15:27:15 +0100490
Richard Felkl66a242d2018-01-25 15:27:15 +0100491/**
492 * Retry commands passed to body
493 *
494 * @param times Number of retries
Jakub Josef61463c72018-02-13 16:10:56 +0100495 * @param delay Delay between retries (in seconds)
Richard Felkl66a242d2018-01-25 15:27:15 +0100496 * @param body Commands to be in retry block
497 * @return calling commands in body
azvyagintsev6bda9422018-08-20 11:57:05 +0300498 * @example retry ( 3 , 5 ) { function body }* retry{ function body }
Richard Felkl66a242d2018-01-25 15:27:15 +0100499 */
500
501def retry(int times = 5, int delay = 0, Closure body) {
502 int retries = 0
503 def exceptions = []
azvyagintsev6bda9422018-08-20 11:57:05 +0300504 while (retries++ < times) {
Richard Felkl66a242d2018-01-25 15:27:15 +0100505 try {
506 return body.call()
azvyagintsev6bda9422018-08-20 11:57:05 +0300507 } catch (e) {
Richard Felkl66a242d2018-01-25 15:27:15 +0100508 sleep(delay)
509 }
510 }
511 currentBuild.result = "FAILURE"
512 throw new Exception("Failed after $times retries")
513}
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300514
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300515/**
516 * Wait for user input with timeout
517 *
518 * @param timeoutInSeconds Timeout
519 * @param options Options for input widget
520 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300521def waitForInputThenPass(timeoutInSeconds, options = [message: 'Ready to go?']) {
522 def userInput = true
523 try {
524 timeout(time: timeoutInSeconds, unit: 'SECONDS') {
525 userInput = input options
526 }
527 } catch (err) { // timeout reached or input false
528 def user = err.getCauses()[0].getUser()
529 if ('SYSTEM' == user.toString()) { // SYSTEM means timeout.
530 println("Timeout, proceeding")
531 } else {
532 userInput = false
533 println("Aborted by: [${user}]")
534 throw err
535 }
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300536 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300537 return userInput
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300538}
Oleksii Grudev9e1d97a2018-06-29 16:04:30 +0300539
540/**
541 * Function receives Map variable as input and sorts it
542 * by values ascending. Returns sorted Map
543 * @param _map Map variable
544 */
545@NonCPS
546def SortMapByValueAsc(_map) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300547 def sortedMap = _map.sort { it.value }
Oleksii Grudev9e1d97a2018-06-29 16:04:30 +0300548 return sortedMap
549}
azvyagintsev99d35842018-08-17 20:26:34 +0300550
551/**
552 * Compare 'old' and 'new' dir's recursively
553 * @param diffData =' Only in new/XXX/infra: secrets.yml
554 Files old/XXX/init.yml and new/XXX/init.yml differ
555 Only in old/XXX/infra: secrets11.yml '
556 *
557 * @return
558 * - new:
559 - XXX/secrets.yml
560 - diff:
561 - XXX/init.yml
562 - removed:
563 - XXX/secrets11.yml
564
565 */
566def diffCheckMultidir(diffData) {
azvyagintsevab5637b2018-08-20 18:18:15 +0300567 common = new com.mirantis.mk.Common()
568 // Some global constants. Don't change\move them!
569 keyNew = 'new'
570 keyRemoved = 'removed'
571 keyDiff = 'diff'
572 def output = [
573 new : [],
574 removed: [],
575 diff : [],
576 ]
577 String pathSep = '/'
578 diffData.each { line ->
579 def job_file = ''
580 def job_type = ''
581 if (line.startsWith('Files old/')) {
582 job_file = new File(line.replace('Files old/', '').tokenize()[0])
583 job_type = keyDiff
584 } else if (line.startsWith('Only in new/')) {
585 // get clean normalized filepath, under new/
586 job_file = new File(line.replace('Only in new/', '').replace(': ', pathSep)).toString()
587 job_type = keyNew
588 } else if (line.startsWith('Only in old/')) {
589 // get clean normalized filepath, under old/
590 job_file = new File(line.replace('Only in old/', '').replace(': ', pathSep)).toString()
591 job_type = keyRemoved
592 } else {
593 common.warningMsg("Not parsed diff line: ${line}!")
594 }
595 if (job_file != '') {
596 output[job_type].push(job_file)
597 }
azvyagintsev99d35842018-08-17 20:26:34 +0300598 }
azvyagintsevab5637b2018-08-20 18:18:15 +0300599 return output
azvyagintsev99d35842018-08-17 20:26:34 +0300600}
601
602/**
603 * Compare 2 folder, file by file
604 * Structure should be:
605 * ${compRoot}/
606 └── diff - diff results will be save here
607 ├── new - input folder with data
608 ├── old - input folder with data
609 ├── pillar.diff - globall diff will be saved here
610 * b_url - usual env.BUILD_URL, to be add into description
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300611 * grepOpts - General grep cmdline; Could be used to pass some magic
612 * regexp into after-diff listing file(pillar.diff)
613 * Example: '-Ev infra/secrets.yml'
azvyagintsev99d35842018-08-17 20:26:34 +0300614 * return - html-based string
615 * TODO: allow to specify subdir for results?
azvyagintsev99d35842018-08-17 20:26:34 +0300616 **/
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300617def comparePillars(compRoot, b_url, grepOpts) {
azvyagintsevab5637b2018-08-20 18:18:15 +0300618 common = new com.mirantis.mk.Common()
azvyagintsevab5637b2018-08-20 18:18:15 +0300619 // Some global constants. Don't change\move them!
620 keyNew = 'new'
621 keyRemoved = 'removed'
622 keyDiff = 'diff'
623 def diff_status = 0
624 // FIXME
625 httpWS = b_url + '/artifact/'
626 dir(compRoot) {
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300627 // If diff empty - exit 0
628 diff_status = sh(script: 'diff -q -r old/ new/ > pillar.diff',
azvyagintsevab5637b2018-08-20 18:18:15 +0300629 returnStatus: true,
630 )
azvyagintsev24d49652018-08-21 19:33:51 +0300631 }
azvyagintsevc0fe1442018-08-21 20:01:34 +0300632 // Unfortunately, diff not able to work with dir-based regexp
633 if (diff_status == 1 && grepOpts) {
634 dir(compRoot) {
635 grep_status = sh(script: """
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300636 cp -v pillar.diff pillar_orig.diff
637 grep ${grepOpts} pillar_orig.diff > pillar.diff
638 """,
azvyagintsevc0fe1442018-08-21 20:01:34 +0300639 returnStatus: true
640 )
azvyagintsevf6e77912018-09-07 15:41:09 +0300641 if (grep_status == 1) {
azvyagintsevc0fe1442018-08-21 20:01:34 +0300642 common.warningMsg("Grep regexp ${grepOpts} removed all diff!")
643 diff_status = 0
azvyagintsevb99f87c2018-08-21 19:43:59 +0300644 }
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300645 }
azvyagintsevc0fe1442018-08-21 20:01:34 +0300646 }
647 // Set job description
648 String description = ''
649 if (diff_status == 1) {
azvyagintsevab5637b2018-08-20 18:18:15 +0300650 // Analyse output file and prepare array with results
651 String data_ = readFile file: "${compRoot}/pillar.diff"
652 def diff_list = diffCheckMultidir(data_.split("\\r?\\n"))
653 common.infoMsg(diff_list)
654 dir(compRoot) {
655 if (diff_list[keyDiff].size() > 0) {
656 if (!fileExists('diff')) {
657 sh('mkdir -p diff')
658 }
659 description += '<b>CHANGED</b><ul>'
660 common.infoMsg('Changed items:')
661 for (item in diff_list[keyDiff]) {
662 // We don't want to handle sub-dirs structure. So, simply make diff 'flat'
663 item_f = item.toString().replace('/', '_')
664 description += "<li><a href=\"${httpWS}/diff/${item_f}/*view*/\">${item}</a></li>"
665 // Generate diff file
666 def diff_exit_code = sh([
667 script : "diff -U 50 old/${item} new/${item} > diff/${item_f}",
668 returnStdout: false,
669 returnStatus: true,
670 ])
671 // catch normal errors, diff should always return 1
672 if (diff_exit_code != 1) {
673 error 'Error with diff file generation'
674 }
675 }
676 }
677 if (diff_list[keyNew].size() > 0) {
678 description += '<b>ADDED</b><ul>'
679 for (item in diff_list[keyNew]) {
680 description += "<li><a href=\"${httpWS}/new/${item}/*view*/\">${item}</a></li>"
681 }
682 }
683 if (diff_list[keyRemoved].size() > 0) {
684 description += '<b>DELETED</b><ul>'
685 for (item in diff_list[keyRemoved]) {
686 description += "<li><a href=\"${httpWS}/old/${item}/*view*/\">${item}</a></li>"
687 }
688 }
azvyagintsev99d35842018-08-17 20:26:34 +0300689
azvyagintsevab5637b2018-08-20 18:18:15 +0300690 }
azvyagintsev99d35842018-08-17 20:26:34 +0300691 }
azvyagintsevab5637b2018-08-20 18:18:15 +0300692
693 if (description != '') {
694 dir(compRoot) {
695 archiveArtifacts([
696 artifacts : '**',
697 allowEmptyArchive: true,
698 ])
699 }
700 return description.toString()
701 } else {
702 return 'No job changes'
703 }
azvyagintsev99d35842018-08-17 20:26:34 +0300704}
azvyagintsevab5637b2018-08-20 18:18:15 +0300705
706/**
707 * Simple function, to get basename from string.
708 * line - path-string
709 * remove_ext - string, optionl. Drop file extenstion.
710 **/
711def GetBaseName(line, remove_ext) {
712 filename = line.toString().split('/').last()
713 if (remove_ext && filename.endsWith(remove_ext.toString())) {
714 filename = filename.take(filename.lastIndexOf(remove_ext.toString()))
715 }
716 return filename
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300717}
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300718
719/**
azvyagintsevf6e77912018-09-07 15:41:09 +0300720 * Return colored string of specific stage in stageMap
721 *
722 * @param stageMap LinkedHashMap object.
723 * @param stageName The name of current stage we are going to execute.
724 * @param color Text color
725 * */
726def getColoredStageView(stageMap, stageName, color) {
727 def stage = stageMap[stageName]
728 def banner = []
729 def currentStageIndex = new ArrayList<String>(stageMap.keySet()).indexOf(stageName)
730 def numberOfStages = stageMap.keySet().size() - 1
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300731
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300732 banner.add(getColorizedString(
azvyagintsevf6e77912018-09-07 15:41:09 +0300733 "=========== Stage ${currentStageIndex}/${numberOfStages}: ${stageName} ===========", color))
734 for (stage_item in stage.keySet()) {
735 banner.add(getColorizedString(
736 "${stage_item}: ${stage[stage_item]}", color))
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300737 }
azvyagintsevf6e77912018-09-07 15:41:09 +0300738 banner.add('\n')
739
740 return banner
741}
742
743/**
744 * Pring stageMap to console with specified color
745 *
746 * @param stageMap LinkedHashMap object with stages information.
747 * @param currentStage The name of current stage we are going to execute.
748 *
749 * */
750def printCurrentStage(stageMap, currentStage) {
751 print getColoredStageView(stageMap, currentStage, "cyan").join('\n')
752}
753
754/**
755 * Pring stageMap to console with specified color
756 *
757 * @param stageMap LinkedHashMap object.
758 * @param baseColor Text color (default white)
759 * */
760def printStageMap(stageMap, baseColor = "white") {
761 def banner = []
762 def index = 0
763 for (stage_name in stageMap.keySet()) {
764 banner.addAll(getColoredStageView(stageMap, stage_name, baseColor))
765 }
766 print banner.join('\n')
767}
768
769/**
770 * Wrap provided code in stage, and do interactive retires if needed.
771 *
772 * @param stageMap LinkedHashMap object with stages information.
773 * @param currentStage The name of current stage we are going to execute.
774 * @param target Target host to execute stage on.
775 * @param interactive Boolean flag to specify if interaction with user is enabled.
776 * @param body Command to be in stage block.
777 * */
778def stageWrapper(stageMap, currentStage, target, interactive = true, Closure body) {
779 def common = new com.mirantis.mk.Common()
780 def banner = []
781
782 printCurrentStage(stageMap, currentStage)
783
784 stage(currentStage) {
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300785 if (interactive){
azvyagintsevf6e77912018-09-07 15:41:09 +0300786 input message: getColorizedString("We are going to execute stage \'${currentStage}\' on the following target ${target}.\nPlease review stage information above.", "yellow")
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300787 }
788 try {
789 return body.call()
790 stageMap[currentStage]['Status'] = "SUCCESS"
791 } catch (Exception err) {
792 def msg = "Stage ${currentStage} failed with the following exception:\n${err}"
793 print getColorizedString(msg, "yellow")
794 common.errorMsg(err)
795 if (interactive) {
796 input message: getColorizedString("Please make sure problem is fixed to proceed with retry. Ready to proceed?", "yellow")
797 stageMap[currentStage]['Status'] = "RETRYING"
798 stageWrapper(stageMap, currentStage, target, interactive, body)
799 } else {
800 error(msg)
azvyagintsevf6e77912018-09-07 15:41:09 +0300801 }
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300802 }
azvyagintsevf6e77912018-09-07 15:41:09 +0300803 }
804}
805
806/**
807 * Ugly transition solution for internal tests.
808 * 1) Check input => transform to static result, based on runtime and input
809 * 2) Check remote-binary repo for exact resource
810 */
811
812def checkRemoteBinary(LinkedHashMap config, List extraScmExtensions = []) {
813 def common = new com.mirantis.mk.Common()
814 res = [:]
815 res['MirrorRoot'] = config.get('globalMirrorRoot', env["BIN_MIRROR_ROOT"] ? env["BIN_MIRROR_ROOT"] : "http://mirror.mirantis.com/")
816 // Reclass-like format's. To make life eazy!
817 res['apt_mk_version'] = config.get('apt_mk_version', env["BIN_APT_MK_VERSION"] ? env["BIN_APT_MK_VERSION"] : 'nightly')
818 res['linux_system_repo_url'] = config.get('linux_system_repo_url', env["BIN_linux_system_repo_url"] ? env["BIN_linux_system_repo_url"] : "${res['MirrorRoot']}/${res['apt_mk_version']}/")
819
820 if (config.get('verify', true)) {
821 MirrorRootStatus = sh(script: "wget --auth-no-challenge --spider ${res['linux_system_repo_url']} 2>/dev/null", returnStatus: true)
822 if (MirrorRootStatus != '0') {
823 common.warningMsg("Resource: ${res['linux_system_repo_url']} not exist")
824 res['linux_system_repo_url'] = false
825 }
826 }
827 return res
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300828}