blob: bf74448cd38db90657db21c54267724e1d5e645d [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
Dmitry Teselkin4badf832020-05-27 17:48:36 +03009import org.jenkinsci.plugins.workflow.cps.EnvActionImpl
10
Jakub Josef79ecec32017-02-17 14:36:28 +010011/**
12 *
13 * Common functions
14 *
15 */
16
17/**
18 * Generate current timestamp
19 *
azvyagintsev6bda9422018-08-20 11:57:05 +030020 * @param format Defaults to yyyyMMddHHmmss
Jakub Josef79ecec32017-02-17 14:36:28 +010021 */
azvyagintsev6bda9422018-08-20 11:57:05 +030022def getDatetime(format = "yyyyMMddHHmmss") {
Jakub Josef79ecec32017-02-17 14:36:28 +010023 def now = new Date();
24 return now.format(format, TimeZone.getTimeZone('UTC'));
25}
26
27/**
Jakub Josef79ecec32017-02-17 14:36:28 +010028 * Return workspace.
29 * Currently implemented by calling pwd so it won't return relevant result in
30 * dir context
31 */
azvyagintsev6bda9422018-08-20 11:57:05 +030032def getWorkspace(includeBuildNum = false) {
Jakub Josef79ecec32017-02-17 14:36:28 +010033 def workspace = sh script: 'pwd', returnStdout: true
34 workspace = workspace.trim()
azvyagintsev6bda9422018-08-20 11:57:05 +030035 if (includeBuildNum) {
36 if (!workspace.endsWith("/")) {
37 workspace += "/"
38 }
39 workspace += env.BUILD_NUMBER
Jakub Josefa661b8c2018-01-17 14:51:25 +010040 }
Jakub Josef79ecec32017-02-17 14:36:28 +010041 return workspace
42}
43
44/**
Dmitry Teselkind4adf972020-02-13 18:24:59 +030045 * Get absolute path via 'realink'
46 * -m, --canonicalize-missing
47 * canonicalize by following every symlink in every component of the given name recursively,
48 * without requirements on components existence
49 */
50def getAbsolutePath(String path) {
51 def absPath = sh script: "readlink -m ${path}", returnStdout: true
52 return absPath.trim()
53}
54
55/**
Filip Pytloun81c864d2017-03-21 15:19:30 +010056 * Get UID of jenkins user.
57 * Must be run from context of node
58 */
59def getJenkinsUid() {
azvyagintsev6bda9422018-08-20 11:57:05 +030060 return sh(
Filip Pytloun81c864d2017-03-21 15:19:30 +010061 script: 'id -u',
62 returnStdout: true
63 ).trim()
64}
65
66/**
67 * Get GID of jenkins user.
68 * Must be run from context of node
69 */
70def getJenkinsGid() {
azvyagintsev6bda9422018-08-20 11:57:05 +030071 return sh(
Filip Pytloun81c864d2017-03-21 15:19:30 +010072 script: 'id -g',
73 returnStdout: true
74 ).trim()
75}
76
77/**
Jakub Josefc8074db2018-01-30 13:33:20 +010078 * Returns Jenkins user uid and gid in one list (in that order)
79 * Must be run from context of node
80 */
azvyagintsev6bda9422018-08-20 11:57:05 +030081def getJenkinsUserIds() {
Jakub Josefc8074db2018-01-30 13:33:20 +010082 return sh(script: "id -u && id -g", returnStdout: true).tokenize("\n")
83}
84
85/**
Alexander Evseevbc1fea42017-12-13 10:03:03 +010086 *
87 * Find credentials by ID
88 *
azvyagintsev6bda9422018-08-20 11:57:05 +030089 * @param credsId Credentials ID
90 * @param credsType Credentials type (optional)
Alexander Evseevbc1fea42017-12-13 10:03:03 +010091 *
92 */
93def getCredentialsById(String credsId, String credsType = 'any') {
94 def credClasses = [ // ordered by class name
azvyagintsev6bda9422018-08-20 11:57:05 +030095 sshKey : com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey.class,
96 cert : com.cloudbees.plugins.credentials.common.CertificateCredentials.class,
97 password : com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials.class,
98 any : com.cloudbees.plugins.credentials.impl.BaseStandardCredentials.class,
99 dockerCert: org.jenkinsci.plugins.docker.commons.credentials.DockerServerCredentials.class,
100 file : org.jenkinsci.plugins.plaincredentials.FileCredentials.class,
101 string : org.jenkinsci.plugins.plaincredentials.StringCredentials.class,
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100102 ]
103 return com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
104 credClasses[credsType],
105 jenkins.model.Jenkins.instance
azvyagintsev6bda9422018-08-20 11:57:05 +0300106 ).findAll { cred -> cred.id == credsId }[0]
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100107}
108
109/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100110 * Get credentials from store
111 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300112 * @param id Credentials name
Jakub Josef79ecec32017-02-17 14:36:28 +0100113 */
Jakub Josef3d9d9ab2017-03-14 15:09:03 +0100114def getCredentials(id, cred_type = "username_password") {
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100115 warningMsg('You are using obsolete function. Please switch to use `getCredentialsById()`')
Jakub Josef79ecec32017-02-17 14:36:28 +0100116
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100117 type_map = [
118 username_password: 'password',
azvyagintsev6bda9422018-08-20 11:57:05 +0300119 key : 'sshKey',
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100120 ]
Jakub Josef79ecec32017-02-17 14:36:28 +0100121
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100122 return getCredentialsById(id, type_map[cred_type])
Jakub Josef79ecec32017-02-17 14:36:28 +0100123}
124
125/**
126 * Abort build, wait for some time and ensure we will terminate
127 */
128def abortBuild() {
129 currentBuild.build().doStop()
130 sleep(180)
131 // just to be sure we will terminate
132 throw new InterruptedException()
133}
134
135/**
Jakub Josefbceaa322017-06-13 18:28:27 +0200136 * Print pretty-printed string representation of given item
137 * @param item item to be pretty-printed (list, map, whatever)
138 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300139def prettyPrint(item) {
Jakub Josefbceaa322017-06-13 18:28:27 +0200140 println prettify(item)
141}
142
143/**
Jakub Josefb41c8d52017-03-24 13:52:24 +0100144 * Return pretty-printed string representation of given item
145 * @param item item to be pretty-printed (list, map, whatever)
146 * @return pretty-printed string
147 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300148def prettify(item) {
Jakub Josefbceaa322017-06-13 18:28:27 +0200149 return groovy.json.JsonOutput.prettyPrint(toJson(item)).replace('\\n', System.getProperty('line.separator'))
Jakub Josefb41c8d52017-03-24 13:52:24 +0100150}
151
152/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100153 * Print informational message
154 *
155 * @param msg
156 * @param color Colorful output or not
157 */
158def infoMsg(msg, color = true) {
159 printMsg(msg, "cyan")
160}
161
162/**
Sergey Galkin7137ad02019-11-07 14:52:13 +0400163 * Print informational message
164 *
165 * @param msg
166 * @param color Colorful output or not
167 */
168def infoSensitivityMsg(msg, color = true, replacing = []) {
169 printSensitivityMsg(msg, "cyan", replacing)
170}
171
172/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100173 * Print error message
174 *
175 * @param msg
176 * @param color Colorful output or not
177 */
178def errorMsg(msg, color = true) {
179 printMsg(msg, "red")
180}
181
182/**
183 * Print success message
184 *
185 * @param msg
186 * @param color Colorful output or not
187 */
188def successMsg(msg, color = true) {
189 printMsg(msg, "green")
190}
191
192/**
193 * Print warning message
194 *
195 * @param msg
196 * @param color Colorful output or not
197 */
198def warningMsg(msg, color = true) {
Jakub Josef0e7bd632017-03-16 16:25:05 +0100199 printMsg(msg, "yellow")
Jakub Josef79ecec32017-02-17 14:36:28 +0100200}
201
202/**
Jakub Josef952ae0b2017-03-14 19:04:21 +0100203 * Print debug message, this message will show only if DEBUG global variable is present
204 * @param msg
205 * @param color Colorful output or not
206 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300207def debugMsg(msg, color = true) {
Jakub Josef9a836ac2017-04-24 12:26:02 +0200208 // if debug property exists on env, debug is enabled
azvyagintsev6bda9422018-08-20 11:57:05 +0300209 if (env.getEnvironment().containsKey('DEBUG') && env['DEBUG'] == "true") {
Jakub Josef74b34692017-03-15 12:10:57 +0100210 printMsg("[DEBUG] ${msg}", "red")
Jakub Josef952ae0b2017-03-14 19:04:21 +0100211 }
212}
213
azvyagintsevf6e77912018-09-07 15:41:09 +0300214def getColorizedString(msg, color) {
215 def colorMap = [
216 'red' : '\u001B[31m',
217 'black' : '\u001B[30m',
218 'green' : '\u001B[32m',
219 'yellow': '\u001B[33m',
220 'blue' : '\u001B[34m',
221 'purple': '\u001B[35m',
222 'cyan' : '\u001B[36m',
223 'white' : '\u001B[37m',
224 'reset' : '\u001B[0m'
225 ]
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300226
azvyagintsevf6e77912018-09-07 15:41:09 +0300227 return "${colorMap[color]}${msg}${colorMap.reset}"
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300228}
229
Jakub Josef952ae0b2017-03-14 19:04:21 +0100230/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100231 * Print message
232 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300233 * @param msg Message to be printed
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300234 * @param color Color to use for output
Jakub Josef79ecec32017-02-17 14:36:28 +0100235 */
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300236def printMsg(msg, color) {
237 print getColorizedString(msg, color)
Jakub Josef79ecec32017-02-17 14:36:28 +0100238}
239
240/**
Sergey Galkin7137ad02019-11-07 14:52:13 +0400241 * Print sensitivity message
242 *
243 * @param msg Message to be printed
244 * @param color Color to use for output
245 * @param replacing List with maps for deletion (passwords, logins, etc).
246 * The first () matching is mandatory !
247 * Example:
248 * [/ (OS_PASSWORD=)(.*?)+ /,
249 * / (password = )(.*?)+ /,
250 * / (password )(.*?) / ]
251 */
252def printSensitivityMsg(msg, color, replacing = []) {
253 for (i in replacing) {
254 msg = msg.replaceAll(i, ' $1XXXXXX ')
255 }
256 printMsg(msg, color)
257}
258
259/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100260 * Traverse directory structure and return list of files
261 *
262 * @param path Path to search
263 * @param type Type of files to search (groovy.io.FileType.FILES)
264 */
265@NonCPS
azvyagintsev6bda9422018-08-20 11:57:05 +0300266def getFiles(path, type = groovy.io.FileType.FILES) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100267 files = []
268 new File(path).eachFile(type) {
269 files[] = it
270 }
271 return files
272}
273
274/**
275 * Helper method to convert map into form of list of [key,value] to avoid
276 * unserializable exceptions
277 *
278 * @param m Map
279 */
280@NonCPS
281def entries(m) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300282 m.collect { k, v -> [k, v] }
Jakub Josef79ecec32017-02-17 14:36:28 +0100283}
284
285/**
286 * Opposite of build-in parallel, run map of steps in serial
287 *
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200288 * @param steps Map of String<name>: CPSClosure2<step> (or list of closures)
Jakub Josef79ecec32017-02-17 14:36:28 +0100289 */
290def serial(steps) {
291 stepsArray = entries(steps)
azvyagintsev6bda9422018-08-20 11:57:05 +0300292 for (i = 0; i < stepsArray.size; i++) {
Jakub Josefd31de302017-05-15 13:59:18 +0200293 def step = stepsArray[i]
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200294 def dummySteps = [:]
295 def stepKey
azvyagintsev6bda9422018-08-20 11:57:05 +0300296 if (step[1] instanceof List || step[1] instanceof Map) {
297 for (j = 0; j < step[1].size(); j++) {
298 if (step[1] instanceof List) {
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200299 stepKey = j
azvyagintsev6bda9422018-08-20 11:57:05 +0300300 } else if (step[1] instanceof Map) {
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200301 stepKey = step[1].keySet()[j]
302 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300303 dummySteps.put("step-${step[0]}-${stepKey}", step[1][stepKey])
Jakub Josefd31de302017-05-15 13:59:18 +0200304 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300305 } else {
Jakub Josefd31de302017-05-15 13:59:18 +0200306 dummySteps.put(step[0], step[1])
307 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100308 parallel dummySteps
309 }
310}
311
312/**
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200313 * Partition given list to list of small lists
314 * @param inputList input list
315 * @param partitionSize (partition size, optional, default 5)
316 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300317def partitionList(inputList, partitionSize = 5) {
318 List<List<String>> partitions = new ArrayList<>();
319 for (int i = 0; i < inputList.size(); i += partitionSize) {
320 partitions.add(new ArrayList<String>(inputList.subList(i, Math.min(i + partitionSize, inputList.size()))));
321 }
322 return partitions
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200323}
324
325/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100326 * Get password credentials from store
327 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300328 * @param id Credentials name
Jakub Josef79ecec32017-02-17 14:36:28 +0100329 */
330def getPasswordCredentials(id) {
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100331 return getCredentialsById(id, 'password')
Jakub Josef79ecec32017-02-17 14:36:28 +0100332}
333
334/**
335 * Get SSH credentials from store
336 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300337 * @param id Credentials name
Jakub Josef79ecec32017-02-17 14:36:28 +0100338 */
339def getSshCredentials(id) {
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100340 return getCredentialsById(id, 'sshKey')
Jakub Josef79ecec32017-02-17 14:36:28 +0100341}
Jakub Josef79ecec32017-02-17 14:36:28 +0100342
343/**
344 * Tests Jenkins instance for existence of plugin with given name
345 * @param pluginName plugin short name to test
346 * @return boolean result
347 */
348@NonCPS
azvyagintsev6bda9422018-08-20 11:57:05 +0300349def jenkinsHasPlugin(pluginName) {
350 return Jenkins.instance.pluginManager.plugins.collect { p -> p.shortName }.contains(pluginName)
Jakub Josef79ecec32017-02-17 14:36:28 +0100351}
352
353@NonCPS
354def _needNotification(notificatedTypes, buildStatus, jobName) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300355 if (notificatedTypes && notificatedTypes.contains("onchange")) {
356 if (jobName) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100357 def job = Jenkins.instance.getItem(jobName)
358 def numbuilds = job.builds.size()
azvyagintsev6bda9422018-08-20 11:57:05 +0300359 if (numbuilds > 0) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100360 //actual build is first for some reasons, so last finished build is second
361 def lastBuild = job.builds[1]
azvyagintsev6bda9422018-08-20 11:57:05 +0300362 if (lastBuild) {
363 if (lastBuild.result.toString().toLowerCase().equals(buildStatus)) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100364 println("Build status didn't changed since last build, not sending notifications")
365 return false;
366 }
367 }
368 }
369 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300370 } else if (!notificatedTypes.contains(buildStatus)) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100371 return false;
372 }
373 return true;
374}
375
376/**
377 * Send notification to all enabled notifications services
378 * @param buildStatus message type (success, warning, error), null means SUCCESSFUL
379 * @param msgText message text
380 * @param enabledNotifications list of enabled notification types, types: slack, hipchat, email, default empty
381 * @param notificatedTypes types of notifications will be sent, default onchange - notificate if current build result not equal last result;
382 * otherwise use - ["success","unstable","failed"]
383 * @param jobName optional job name param, if empty env.JOB_NAME will be used
Jakub Josefd0571152017-07-17 14:11:39 +0200384 * @param buildNumber build number param, if empty env.BUILD_NUM will be used
385 * @param buildUrl build url param, if empty env.BUILD_URL will be used
Jakub Josef79ecec32017-02-17 14:36:28 +0100386 * @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 +0200387 * @param mailTo mail TO param, it's mandatory for sending email notifications, this option enable mail notification
Jakub Josef79ecec32017-02-17 14:36:28 +0100388 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300389def sendNotification(buildStatus, msgText = "", enabledNotifications = [], notificatedTypes = ["onchange"], jobName = null, buildNumber = null, buildUrl = null, mailFrom = "jenkins", mailTo = null) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100390 // Default values
391 def colorName = 'blue'
392 def colorCode = '#0000FF'
393 def buildStatusParam = buildStatus != null && buildStatus != "" ? buildStatus : "SUCCESS"
394 def jobNameParam = jobName != null && jobName != "" ? jobName : env.JOB_NAME
395 def buildNumberParam = buildNumber != null && buildNumber != "" ? buildNumber : env.BUILD_NUMBER
396 def buildUrlParam = buildUrl != null && buildUrl != "" ? buildUrl : env.BUILD_URL
397 def subject = "${buildStatusParam}: Job '${jobNameParam} [${buildNumberParam}]'"
398 def summary = "${subject} (${buildUrlParam})"
399
azvyagintsev6bda9422018-08-20 11:57:05 +0300400 if (msgText != null && msgText != "") {
401 summary += "\n${msgText}"
Jakub Josef79ecec32017-02-17 14:36:28 +0100402 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300403 if (buildStatusParam.toLowerCase().equals("success")) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100404 colorCode = "#00FF00"
405 colorName = "green"
azvyagintsev6bda9422018-08-20 11:57:05 +0300406 } else if (buildStatusParam.toLowerCase().equals("unstable")) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100407 colorCode = "#FFFF00"
408 colorName = "yellow"
azvyagintsev6bda9422018-08-20 11:57:05 +0300409 } else if (buildStatusParam.toLowerCase().equals("failure")) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100410 colorCode = "#FF0000"
411 colorName = "red"
412 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300413 if (_needNotification(notificatedTypes, buildStatusParam.toLowerCase(), jobNameParam)) {
414 if (enabledNotifications.contains("slack") && jenkinsHasPlugin("slack")) {
415 try {
Jakub Josef79ecec32017-02-17 14:36:28 +0100416 slackSend color: colorCode, message: summary
azvyagintsev6bda9422018-08-20 11:57:05 +0300417 } catch (Exception e) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100418 println("Calling slack plugin failed")
419 e.printStackTrace()
420 }
421 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300422 if (enabledNotifications.contains("hipchat") && jenkinsHasPlugin("hipchat")) {
423 try {
Jakub Josef79ecec32017-02-17 14:36:28 +0100424 hipchatSend color: colorName.toUpperCase(), message: summary
azvyagintsev6bda9422018-08-20 11:57:05 +0300425 } catch (Exception e) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100426 println("Calling hipchat plugin failed")
427 e.printStackTrace()
428 }
429 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300430 if (enabledNotifications.contains("email") && mailTo != null && mailTo != "" && mailFrom != null && mailFrom != "") {
431 try {
Jakub Josef79ecec32017-02-17 14:36:28 +0100432 mail body: summary, from: mailFrom, subject: subject, to: mailTo
azvyagintsev6bda9422018-08-20 11:57:05 +0300433 } catch (Exception e) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100434 println("Sending mail plugin failed")
435 e.printStackTrace()
436 }
437 }
438 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100439}
chnyda4e5ac792017-03-14 15:24:18 +0100440
441/**
442 * Execute linux command and catch nth element
443 * @param cmd command to execute
444 * @param index index to retrieve
445 * @return index-th element
446 */
447
azvyagintsev6bda9422018-08-20 11:57:05 +0300448def cutOrDie(cmd, index) {
chnyda4e5ac792017-03-14 15:24:18 +0100449 def common = new com.mirantis.mk.Common()
450 def output
451 try {
azvyagintsev6bda9422018-08-20 11:57:05 +0300452 output = sh(script: cmd, returnStdout: true)
453 def result = output.tokenize(" ")[index]
454 return result;
chnyda4e5ac792017-03-14 15:24:18 +0100455 } catch (Exception e) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300456 common.errorMsg("Failed to execute cmd: ${cmd}\n output: ${output}")
chnyda4e5ac792017-03-14 15:24:18 +0100457 }
Filip Pytloun81c864d2017-03-21 15:19:30 +0100458}
Tomáš Kukrál767dd732017-03-23 10:38:59 +0100459
460/**
461 * Check variable contains keyword
462 * @param variable keywork is searched (contains) here
463 * @param keyword string to look for
464 * @return True if variable contains keyword (case insensitive), False if do not contains or any of input isn't a string
465 */
466
467def checkContains(variable, keyword) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300468 if (env.getEnvironment().containsKey(variable)) {
Jakub Josef7a8dea22017-03-23 19:51:32 +0100469 return env[variable] && env[variable].toLowerCase().contains(keyword.toLowerCase())
Tomáš Kukrál767dd732017-03-23 10:38:59 +0100470 } else {
Tomáš Kukrálc76c1e02017-03-23 19:06:59 +0100471 return false
Tomáš Kukrál767dd732017-03-23 10:38:59 +0100472 }
473}
Jakub Josefa877db52017-04-05 14:22:30 +0200474
475/**
476 * Parse JSON string to hashmap
477 * @param jsonString input JSON string
478 * @return created hashmap
479 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300480def parseJSON(jsonString) {
481 def m = [:]
482 def lazyMap = new JsonSlurperClassic().parseText(jsonString)
483 m.putAll(lazyMap)
484 return m
Jakub Josefa877db52017-04-05 14:22:30 +0200485}
Jakub Josefed239cd2017-05-09 15:27:33 +0200486
487/**
Vasyl Saienko79d6f3b2018-10-19 09:13:46 +0300488 *
489 * Deep merge of Map items. Merges variable number of maps in to onto.
490 * Using the following rules:
491 * - Lists are appended
492 * - Maps are updated
493 * - other object types are replaced.
494 *
495 *
496 * @param onto Map object to merge in
497 * @param overrides Map objects to merge to onto
498*/
499def mergeMaps(Map onto, Map... overrides){
500 if (!overrides){
501 return onto
502 }
503 else if (overrides.length == 1) {
504 overrides[0]?.each { k, v ->
Vasyl Saienkof92cf4f2019-07-10 12:39:25 +0300505 if (k in onto.keySet()) {
506 if (v in Map && onto[k] in Map){
507 mergeMaps((Map) onto[k], (Map) v)
508 } else if (v in List) {
509 onto[k] += v
510 } else {
511 onto[k] = v
512 }
Vasyl Saienko79d6f3b2018-10-19 09:13:46 +0300513 } else {
514 onto[k] = v
515 }
516 }
517 return onto
518 }
519 return overrides.inject(onto, { acc, override -> mergeMaps(acc, override ?: [:]) })
520}
521
522/**
Jakub Josefed239cd2017-05-09 15:27:33 +0200523 * Test pipeline input parameter existence and validity (not null and not empty string)
524 * @param paramName input parameter name (usually uppercase)
azvyagintsevc8ecdfd2018-09-11 12:47:15 +0300525 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300526def validInputParam(paramName) {
azvyagintsevc8ecdfd2018-09-11 12:47:15 +0300527 if (paramName instanceof java.lang.String) {
528 return env.getEnvironment().containsKey(paramName) && env[paramName] != null && env[paramName] != ""
529 }
530 return false
Tomáš Kukráld34fe872017-06-13 10:50:50 +0200531}
532
533/**
534 * Take list of hashmaps and count number of hashmaps with parameter equals eq
535 * @param lm list of hashmaps
536 * @param param define parameter of hashmap to read and compare
537 * @param eq desired value of hashmap parameter
538 * @return count of hashmaps meeting defined condition
539 */
540
541@NonCPS
542def countHashMapEquals(lm, param, eq) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300543 return lm.stream().filter { i -> i[param].equals(eq) }.collect(java.util.stream.Collectors.counting())
Tomáš Kukráld34fe872017-06-13 10:50:50 +0200544}
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200545
546/**
547 * Execute shell command and return stdout, stderr and status
548 *
549 * @param cmd Command to execute
550 * @return map with stdout, stderr, status keys
551 */
552
553def shCmdStatus(cmd) {
azvyagintsev386e94e2019-06-13 13:39:04 +0300554 // Set +x , to hide odd messages about temp file manipulations
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200555 def res = [:]
azvyagintsev386e94e2019-06-13 13:39:04 +0300556 def stderr = sh(script: 'set +x ; mktemp', returnStdout: true).trim()
557 def stdout = sh(script: 'set +x ; mktemp', returnStdout: true).trim()
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200558
559 try {
azvyagintsev6bda9422018-08-20 11:57:05 +0300560 def status = sh(script: "${cmd} 1>${stdout} 2>${stderr}", returnStatus: true)
azvyagintsev386e94e2019-06-13 13:39:04 +0300561 res['stderr'] = sh(script: "set +x; cat ${stderr}", returnStdout: true).trim()
562 res['stdout'] = sh(script: "set +x; cat ${stdout}", returnStdout: true).trim()
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200563 res['status'] = status
564 } finally {
azvyagintsev386e94e2019-06-13 13:39:04 +0300565 sh(script: "set +x; rm ${stderr}")
566 sh(script: "set +x; rm ${stdout}")
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200567 }
568
569 return res
570}
Richard Felkl66a242d2018-01-25 15:27:15 +0100571
Richard Felkl66a242d2018-01-25 15:27:15 +0100572/**
573 * Retry commands passed to body
574 *
Martin Polreich331f2b62019-02-08 10:16:52 +0100575 * Don't use common.retry method for retrying salt.enforceState method. Use retries parameter
576 * built-in the salt.enforceState method instead to ensure correct functionality.
577 *
Richard Felkl66a242d2018-01-25 15:27:15 +0100578 * @param times Number of retries
Jakub Josef61463c72018-02-13 16:10:56 +0100579 * @param delay Delay between retries (in seconds)
Richard Felkl66a242d2018-01-25 15:27:15 +0100580 * @param body Commands to be in retry block
581 * @return calling commands in body
azvyagintsev6bda9422018-08-20 11:57:05 +0300582 * @example retry ( 3 , 5 ) { function body }* retry{ function body }
Richard Felkl66a242d2018-01-25 15:27:15 +0100583 */
584
585def retry(int times = 5, int delay = 0, Closure body) {
586 int retries = 0
azvyagintsev6bda9422018-08-20 11:57:05 +0300587 while (retries++ < times) {
Richard Felkl66a242d2018-01-25 15:27:15 +0100588 try {
589 return body.call()
azvyagintsev6bda9422018-08-20 11:57:05 +0300590 } catch (e) {
Denis Egorenko900a3af2019-01-14 12:54:56 +0400591 errorMsg(e.toString())
Richard Felkl66a242d2018-01-25 15:27:15 +0100592 sleep(delay)
593 }
594 }
Richard Felkl66a242d2018-01-25 15:27:15 +0100595 throw new Exception("Failed after $times retries")
596}
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300597
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300598/**
599 * Wait for user input with timeout
600 *
601 * @param timeoutInSeconds Timeout
602 * @param options Options for input widget
603 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300604def waitForInputThenPass(timeoutInSeconds, options = [message: 'Ready to go?']) {
605 def userInput = true
606 try {
607 timeout(time: timeoutInSeconds, unit: 'SECONDS') {
608 userInput = input options
609 }
610 } catch (err) { // timeout reached or input false
611 def user = err.getCauses()[0].getUser()
612 if ('SYSTEM' == user.toString()) { // SYSTEM means timeout.
613 println("Timeout, proceeding")
614 } else {
615 userInput = false
616 println("Aborted by: [${user}]")
617 throw err
618 }
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300619 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300620 return userInput
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300621}
Oleksii Grudev9e1d97a2018-06-29 16:04:30 +0300622
623/**
624 * Function receives Map variable as input and sorts it
625 * by values ascending. Returns sorted Map
626 * @param _map Map variable
627 */
628@NonCPS
629def SortMapByValueAsc(_map) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300630 def sortedMap = _map.sort { it.value }
Oleksii Grudev9e1d97a2018-06-29 16:04:30 +0300631 return sortedMap
632}
azvyagintsev99d35842018-08-17 20:26:34 +0300633
634/**
635 * Compare 'old' and 'new' dir's recursively
636 * @param diffData =' Only in new/XXX/infra: secrets.yml
637 Files old/XXX/init.yml and new/XXX/init.yml differ
638 Only in old/XXX/infra: secrets11.yml '
639 *
640 * @return
641 * - new:
642 - XXX/secrets.yml
643 - diff:
644 - XXX/init.yml
645 - removed:
646 - XXX/secrets11.yml
647
648 */
649def diffCheckMultidir(diffData) {
azvyagintsevab5637b2018-08-20 18:18:15 +0300650 common = new com.mirantis.mk.Common()
651 // Some global constants. Don't change\move them!
652 keyNew = 'new'
653 keyRemoved = 'removed'
654 keyDiff = 'diff'
655 def output = [
656 new : [],
657 removed: [],
658 diff : [],
659 ]
660 String pathSep = '/'
661 diffData.each { line ->
662 def job_file = ''
663 def job_type = ''
664 if (line.startsWith('Files old/')) {
665 job_file = new File(line.replace('Files old/', '').tokenize()[0])
666 job_type = keyDiff
667 } else if (line.startsWith('Only in new/')) {
668 // get clean normalized filepath, under new/
669 job_file = new File(line.replace('Only in new/', '').replace(': ', pathSep)).toString()
670 job_type = keyNew
671 } else if (line.startsWith('Only in old/')) {
672 // get clean normalized filepath, under old/
673 job_file = new File(line.replace('Only in old/', '').replace(': ', pathSep)).toString()
674 job_type = keyRemoved
675 } else {
676 common.warningMsg("Not parsed diff line: ${line}!")
677 }
678 if (job_file != '') {
679 output[job_type].push(job_file)
680 }
azvyagintsev99d35842018-08-17 20:26:34 +0300681 }
azvyagintsevab5637b2018-08-20 18:18:15 +0300682 return output
azvyagintsev99d35842018-08-17 20:26:34 +0300683}
684
685/**
686 * Compare 2 folder, file by file
687 * Structure should be:
688 * ${compRoot}/
689 └── diff - diff results will be save here
690 ├── new - input folder with data
691 ├── old - input folder with data
692 ├── pillar.diff - globall diff will be saved here
693 * b_url - usual env.BUILD_URL, to be add into description
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300694 * grepOpts - General grep cmdline; Could be used to pass some magic
695 * regexp into after-diff listing file(pillar.diff)
696 * Example: '-Ev infra/secrets.yml'
azvyagintsev99d35842018-08-17 20:26:34 +0300697 * return - html-based string
698 * TODO: allow to specify subdir for results?
azvyagintsev99d35842018-08-17 20:26:34 +0300699 **/
Denis Egorenko4551f372018-09-11 16:36:13 +0400700
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300701def comparePillars(compRoot, b_url, grepOpts) {
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +0000702
azvyagintsevab5637b2018-08-20 18:18:15 +0300703 // Some global constants. Don't change\move them!
704 keyNew = 'new'
705 keyRemoved = 'removed'
706 keyDiff = 'diff'
707 def diff_status = 0
708 // FIXME
709 httpWS = b_url + '/artifact/'
710 dir(compRoot) {
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300711 // If diff empty - exit 0
712 diff_status = sh(script: 'diff -q -r old/ new/ > pillar.diff',
azvyagintsevab5637b2018-08-20 18:18:15 +0300713 returnStatus: true,
714 )
azvyagintsev24d49652018-08-21 19:33:51 +0300715 }
azvyagintsevc0fe1442018-08-21 20:01:34 +0300716 // Unfortunately, diff not able to work with dir-based regexp
717 if (diff_status == 1 && grepOpts) {
718 dir(compRoot) {
719 grep_status = sh(script: """
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300720 cp -v pillar.diff pillar_orig.diff
721 grep ${grepOpts} pillar_orig.diff > pillar.diff
722 """,
azvyagintsevc0fe1442018-08-21 20:01:34 +0300723 returnStatus: true
724 )
azvyagintsevf6e77912018-09-07 15:41:09 +0300725 if (grep_status == 1) {
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +0000726 warningMsg("Grep regexp ${grepOpts} removed all diff!")
azvyagintsevc0fe1442018-08-21 20:01:34 +0300727 diff_status = 0
azvyagintsevb99f87c2018-08-21 19:43:59 +0300728 }
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300729 }
azvyagintsevc0fe1442018-08-21 20:01:34 +0300730 }
731 // Set job description
Denis Egorenko4551f372018-09-11 16:36:13 +0400732 description = ''
azvyagintsevc0fe1442018-08-21 20:01:34 +0300733 if (diff_status == 1) {
azvyagintsevab5637b2018-08-20 18:18:15 +0300734 // Analyse output file and prepare array with results
735 String data_ = readFile file: "${compRoot}/pillar.diff"
736 def diff_list = diffCheckMultidir(data_.split("\\r?\\n"))
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +0000737 infoMsg(diff_list)
azvyagintsevab5637b2018-08-20 18:18:15 +0300738 dir(compRoot) {
739 if (diff_list[keyDiff].size() > 0) {
740 if (!fileExists('diff')) {
741 sh('mkdir -p diff')
742 }
743 description += '<b>CHANGED</b><ul>'
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +0000744 infoMsg('Changed items:')
Denis Egorenko4551f372018-09-11 16:36:13 +0400745 def stepsForParallel = [:]
746 stepsForParallel.failFast = true
747 diff_list[keyDiff].each {
748 stepsForParallel.put("Differ for:${it}",
749 {
750 // We don't want to handle sub-dirs structure. So, simply make diff 'flat'
751 def item_f = it.toString().replace('/', '_')
752 description += "<li><a href=\"${httpWS}/diff/${item_f}/*view*/\">${it}</a></li>"
753 // Generate diff file
754 def diff_exit_code = sh([
755 script : "diff -U 50 old/${it} new/${it} > diff/${item_f}",
756 returnStdout: false,
757 returnStatus: true,
758 ])
759 // catch normal errors, diff should always return 1
760 if (diff_exit_code != 1) {
761 error 'Error with diff file generation'
762 }
763 })
azvyagintsevab5637b2018-08-20 18:18:15 +0300764 }
Denis Egorenko4551f372018-09-11 16:36:13 +0400765
766 parallel stepsForParallel
azvyagintsevab5637b2018-08-20 18:18:15 +0300767 }
768 if (diff_list[keyNew].size() > 0) {
769 description += '<b>ADDED</b><ul>'
770 for (item in diff_list[keyNew]) {
771 description += "<li><a href=\"${httpWS}/new/${item}/*view*/\">${item}</a></li>"
772 }
773 }
774 if (diff_list[keyRemoved].size() > 0) {
775 description += '<b>DELETED</b><ul>'
776 for (item in diff_list[keyRemoved]) {
777 description += "<li><a href=\"${httpWS}/old/${item}/*view*/\">${item}</a></li>"
778 }
779 }
Denis Egorenko62120962019-03-15 11:24:32 +0400780 def cwd = sh(script: 'basename $(pwd)', returnStdout: true).trim()
781 sh "tar -cf old_${cwd}.tar.gz old/ && rm -rf old/"
782 sh "tar -cf new_${cwd}.tar.gz new/ && rm -rf new/"
azvyagintsevab5637b2018-08-20 18:18:15 +0300783 }
azvyagintsev99d35842018-08-17 20:26:34 +0300784 }
azvyagintsevab5637b2018-08-20 18:18:15 +0300785
786 if (description != '') {
787 dir(compRoot) {
788 archiveArtifacts([
789 artifacts : '**',
790 allowEmptyArchive: true,
791 ])
792 }
793 return description.toString()
794 } else {
azvyagintseva57c82a2018-09-20 12:17:24 +0300795 return '<b>No job changes</b>'
azvyagintsevab5637b2018-08-20 18:18:15 +0300796 }
azvyagintsev99d35842018-08-17 20:26:34 +0300797}
azvyagintsevab5637b2018-08-20 18:18:15 +0300798
799/**
800 * Simple function, to get basename from string.
801 * line - path-string
802 * remove_ext - string, optionl. Drop file extenstion.
803 **/
804def GetBaseName(line, remove_ext) {
805 filename = line.toString().split('/').last()
806 if (remove_ext && filename.endsWith(remove_ext.toString())) {
807 filename = filename.take(filename.lastIndexOf(remove_ext.toString()))
808 }
809 return filename
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300810}
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300811
812/**
azvyagintsevf6e77912018-09-07 15:41:09 +0300813 * Return colored string of specific stage in stageMap
814 *
815 * @param stageMap LinkedHashMap object.
816 * @param stageName The name of current stage we are going to execute.
817 * @param color Text color
818 * */
819def getColoredStageView(stageMap, stageName, color) {
820 def stage = stageMap[stageName]
821 def banner = []
822 def currentStageIndex = new ArrayList<String>(stageMap.keySet()).indexOf(stageName)
823 def numberOfStages = stageMap.keySet().size() - 1
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300824
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300825 banner.add(getColorizedString(
azvyagintsevf6e77912018-09-07 15:41:09 +0300826 "=========== Stage ${currentStageIndex}/${numberOfStages}: ${stageName} ===========", color))
827 for (stage_item in stage.keySet()) {
828 banner.add(getColorizedString(
829 "${stage_item}: ${stage[stage_item]}", color))
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300830 }
azvyagintsevf6e77912018-09-07 15:41:09 +0300831 banner.add('\n')
832
833 return banner
834}
835
836/**
837 * Pring stageMap to console with specified color
838 *
839 * @param stageMap LinkedHashMap object with stages information.
840 * @param currentStage The name of current stage we are going to execute.
841 *
842 * */
843def printCurrentStage(stageMap, currentStage) {
844 print getColoredStageView(stageMap, currentStage, "cyan").join('\n')
845}
846
847/**
848 * Pring stageMap to console with specified color
849 *
850 * @param stageMap LinkedHashMap object.
851 * @param baseColor Text color (default white)
852 * */
853def printStageMap(stageMap, baseColor = "white") {
854 def banner = []
855 def index = 0
856 for (stage_name in stageMap.keySet()) {
857 banner.addAll(getColoredStageView(stageMap, stage_name, baseColor))
858 }
859 print banner.join('\n')
860}
861
862/**
863 * Wrap provided code in stage, and do interactive retires if needed.
864 *
865 * @param stageMap LinkedHashMap object with stages information.
866 * @param currentStage The name of current stage we are going to execute.
867 * @param target Target host to execute stage on.
868 * @param interactive Boolean flag to specify if interaction with user is enabled.
869 * @param body Command to be in stage block.
870 * */
871def stageWrapper(stageMap, currentStage, target, interactive = true, Closure body) {
872 def common = new com.mirantis.mk.Common()
873 def banner = []
874
875 printCurrentStage(stageMap, currentStage)
876
877 stage(currentStage) {
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300878 if (interactive){
azvyagintsevf6e77912018-09-07 15:41:09 +0300879 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 +0300880 }
881 try {
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300882 stageMap[currentStage]['Status'] = "SUCCESS"
Victor Ryzhenkin49d67812019-01-09 15:28:21 +0400883 return body.call()
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300884 } catch (Exception err) {
885 def msg = "Stage ${currentStage} failed with the following exception:\n${err}"
886 print getColorizedString(msg, "yellow")
887 common.errorMsg(err)
888 if (interactive) {
889 input message: getColorizedString("Please make sure problem is fixed to proceed with retry. Ready to proceed?", "yellow")
890 stageMap[currentStage]['Status'] = "RETRYING"
891 stageWrapper(stageMap, currentStage, target, interactive, body)
892 } else {
893 error(msg)
azvyagintsevf6e77912018-09-07 15:41:09 +0300894 }
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300895 }
azvyagintsevf6e77912018-09-07 15:41:09 +0300896 }
897}
898
899/**
900 * Ugly transition solution for internal tests.
901 * 1) Check input => transform to static result, based on runtime and input
902 * 2) Check remote-binary repo for exact resource
azvyagintsevf6fce3d2018-12-28 15:30:33 +0200903 * Return: changes each linux_system_* cto false, in case broken url in some of them
904 */
azvyagintsevf6e77912018-09-07 15:41:09 +0300905
906def checkRemoteBinary(LinkedHashMap config, List extraScmExtensions = []) {
907 def common = new com.mirantis.mk.Common()
azvyagintsevf6fce3d2018-12-28 15:30:33 +0200908 def res = [:]
azvyagintsevf6e77912018-09-07 15:41:09 +0300909 res['MirrorRoot'] = config.get('globalMirrorRoot', env["BIN_MIRROR_ROOT"] ? env["BIN_MIRROR_ROOT"] : "http://mirror.mirantis.com/")
910 // Reclass-like format's. To make life eazy!
azvyagintsev603d95b2018-11-09 15:37:10 +0200911 res['mcp_version'] = config.get('mcp_version', env["BIN_APT_MCP_VERSION"] ? env["BIN_APT_MCP_VERSION"] : 'nightly')
azvyagintsevf6fce3d2018-12-28 15:30:33 +0200912 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['mcp_version']}/")
913 res['linux_system_repo_ubuntu_url'] = config.get('linux_system_repo_ubuntu_url', env['BIN_linux_system_repo_ubuntu_url'] ? env['BIN_linux_system_repo_ubuntu_url'] : "${res['MirrorRoot']}/${res['mcp_version']}/ubuntu/")
914 res['linux_system_repo_mcp_salt_url'] = config.get('linux_system_repo_mcp_salt_url', env['BIN_linux_system_repo_mcp_salt_url'] ? env['BIN_linux_system_repo_mcp_salt_url'] : "${res['MirrorRoot']}/${res['mcp_version']}/salt-formulas/")
azvyagintsevf6e77912018-09-07 15:41:09 +0300915
916 if (config.get('verify', true)) {
azvyagintsevf6fce3d2018-12-28 15:30:33 +0200917 res.each { key, val ->
918 if (key.toString().startsWith('linux_system_repo')) {
919 def MirrorRootStatus = sh(script: "wget --auth-no-challenge --spider ${val} 2>/dev/null", returnStatus: true)
920 if (MirrorRootStatus != 0) {
921 common.warningMsg("Resource: '${key}' at '${val}' not exist!")
922 res[key] = false
923 }
924 }
azvyagintsevf6e77912018-09-07 15:41:09 +0300925 }
926 }
927 return res
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300928}
Denis Egorenkoeded0d42018-09-26 13:25:49 +0400929
930/**
931 * Workaround to update env properties, like GERRIT_* vars,
932 * which should be passed from upstream job to downstream.
933 * Will not fail entire job in case any issues.
934 * @param envVar - EnvActionImpl env job
935 * @param extraVars - Multiline YAML text with extra vars
936 */
937def mergeEnv(envVar, extraVars) {
938 try {
939 def extraParams = readYaml text: extraVars
940 for(String key in extraParams.keySet()) {
941 envVar[key] = extraParams[key]
Dmitry Teselkin4badf832020-05-27 17:48:36 +0300942 println("INFO: Parameter ${key} is updated from EXTRA vars.")
Denis Egorenkoeded0d42018-09-26 13:25:49 +0400943 }
944 } catch (Exception e) {
Dmitry Teselkin4badf832020-05-27 17:48:36 +0300945 println("ERR: Can't update env parameteres, because: ${e.toString()}")
Denis Egorenkoeded0d42018-09-26 13:25:49 +0400946 }
947}
Denis Egorenko599bd632018-09-28 15:24:37 +0400948
azvyagintsev898e1742020-05-12 12:40:44 +0300949def setMapDefaults(Object base, Object defaults, Boolean recursive = false) {
950/**
951 * Function to update dict with params, if its not set yet
952 * Will not fail entire job in case any issues.
953 * Those function will not overwrite current options if already passed.
954 * @param base - dict
955 * @param defaults - dict
956 */
957 if (base instanceof Map && defaults instanceof Map) {
958 defaults.inject(base) { result, key, value ->
959 if (result.containsKey(key)) {
960 setMapDefaults(result[key], value, recursive = true)
961 } else {
962 result.put(key, value)
963 }
964 return result
965 }
966 } else if (!recursive) {
967 echo("Can't update map parameters, wrong input data, skipping")
968 }
969}
970
Dmitry Teselkin4badf832020-05-27 17:48:36 +0300971def setEnvDefaults(Object envVar, Object defaults) {
972 /**
973 * Function to set default values of an environment variables object
974 * at runtime(patches existing 'env' instance).
975 * @param env - instance of either EnvActionImpl
976 * @param defaults - Map with default values
977 * Example: setEnvDefaults(env, ['ENV_NAME': 'newENV_NAME', 'newvar': 'newval'])
978 * */
979
980 if (!(envVar instanceof EnvActionImpl)) {
981 error("setEnvDefaults 'env' is not an instance of EnvActionImpl")
982 } else if (!(defaults instanceof Map)) {
983 error("setEnvDefaults 'defaults' is not a Map")
984 }
985 defaults.each { key, value ->
986 if (envVar.getEnvironment().containsKey(key)) {
987 println("INFO:setEnvDefaults env variable ${key} already exist, not overwriting")
988 } else {
989 envVar[key] = value
990 println("INFO:setEnvDefaults env variable ${key} has been added")
991 }
992 }
993}
azvyagintsev898e1742020-05-12 12:40:44 +0300994
Denis Egorenko599bd632018-09-28 15:24:37 +0400995/**
996 * Wrapper around parallel pipeline function
997 * with ability to restrict number of parallel threads
998 * running simultaneously
999 *
1000 * @param branches - Map with Clousers to be executed
1001 * @param maxParallelJob - Integer number of parallel threads allowed
1002 * to run simultaneously
1003 */
1004def runParallel(branches, maxParallelJob = 10) {
1005 def runningSteps = 0
1006 branches.each { branchName, branchBody ->
1007 if (branchBody instanceof Closure) {
1008 branches[branchName] = {
1009 while (!(runningSteps < maxParallelJob)) {
1010 continue
1011 }
1012 runningSteps += 1
1013 branchBody.call()
1014 runningSteps -= 1
1015 }
1016 }
1017 }
1018 if (branches) {
1019 parallel branches
1020 }
1021}
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001022
1023/**
1024 * Ugly processing basic funcs with /etc/apt
Denis Egorenko5cea1412018-10-18 16:40:11 +04001025 * @param repoConfig YAML text or Map
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001026 * Example :
Denis Egorenko5cea1412018-10-18 16:40:11 +04001027 repoConfig = '''
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001028 ---
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001029 aprConfD: |-
Denis Egorenkoe02a1b22018-10-19 17:47:53 +04001030 APT::Get::AllowUnauthenticated 'true';
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001031 repo:
Denis Egorenkoe02a1b22018-10-19 17:47:53 +04001032 mcp_saltstack:
1033 source: "deb [arch=amd64] http://mirror.mirantis.com/nightly/saltstack-2017.7/xenial xenial main"
1034 pin:
1035 - package: "libsodium18"
1036 pin: "release o=SaltStack"
1037 priority: 50
1038 - package: "*"
1039 pin: "release o=SaltStack"
1040 priority: "1100"
1041 repo_key: "http://mirror.mirantis.com/public.gpg"
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001042 '''
1043 *
1044 */
1045
Denis Egorenko5cea1412018-10-18 16:40:11 +04001046def debianExtraRepos(repoConfig) {
1047 def config = null
1048 if (repoConfig instanceof Map) {
1049 config = repoConfig
1050 } else {
1051 config = readYaml text: repoConfig
1052 }
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001053 if (config.get('repo', false)) {
1054 for (String repo in config['repo'].keySet()) {
Denis Egorenko395aa212018-10-11 15:11:28 +04001055 source = config['repo'][repo]['source']
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001056 warningMsg("Write ${source} > /etc/apt/sources.list.d/${repo}.list")
1057 sh("echo '${source}' > /etc/apt/sources.list.d/${repo}.list")
Denis Egorenko395aa212018-10-11 15:11:28 +04001058 if (config['repo'][repo].containsKey('repo_key')) {
1059 key = config['repo'][repo]['repo_key']
1060 sh("wget -O - '${key}' | apt-key add -")
1061 }
Denis Egorenkoe02a1b22018-10-19 17:47:53 +04001062 if (config['repo'][repo]['pin']) {
1063 def repoPins = []
1064 for (Map pin in config['repo'][repo]['pin']) {
1065 repoPins.add("Package: ${pin['package']}")
1066 repoPins.add("Pin: ${pin['pin']}")
1067 repoPins.add("Pin-Priority: ${pin['priority']}")
Denis Egorenko1c93d122018-11-02 12:14:05 +04001068 // additional empty line between pins
1069 repoPins.add('\n')
Denis Egorenkoe02a1b22018-10-19 17:47:53 +04001070 }
1071 if (repoPins) {
1072 repoPins.add(0, "### Extra ${repo} repo pin start ###")
1073 repoPins.add("### Extra ${repo} repo pin end ###")
1074 repoPinning = repoPins.join('\n')
1075 warningMsg("Adding pinning \n${repoPinning}\n => /etc/apt/preferences.d/${repo}")
1076 sh("echo '${repoPinning}' > /etc/apt/preferences.d/${repo}")
1077 }
1078 }
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001079 }
1080 }
1081 if (config.get('aprConfD', false)) {
1082 for (String pref in config['aprConfD'].tokenize('\n')) {
1083 warningMsg("Adding ${pref} => /etc/apt/apt.conf.d/99setupAndTestNode")
1084 sh("echo '${pref}' >> /etc/apt/apt.conf.d/99setupAndTestNode")
1085 }
1086 sh('cat /etc/apt/apt.conf.d/99setupAndTestNode')
1087 }
1088}
Denis Egorenkoeaf78db2019-02-06 17:01:38 +04001089
1090/**
1091 * Parse date from string
1092 * @param String date - date to parse
1093 * @param String format - date format in provided date string value
1094 *
1095 * return new Date() object
1096 */
1097Date parseDate(String date, String format) {
1098 return Date.parse(format, date)
1099}
Denis Egorenko815758d2019-07-08 15:54:08 +04001100
1101/**
1102 * Generate Random Hash string
1103 * @param n Hash length
1104 * @param pool Pool to use for hash generation
1105*/
1106def generateRandomHashString(int n, ArrayList pool = []) {
1107 if (!pool) {
1108 pool = ['a'..'z','A'..'Z',0..9,'_','+','='].flatten()
1109 }
1110 Random rand = new Random(System.currentTimeMillis())
1111 return (1..n).collect { pool[rand.nextInt(pool.size())] }.join()
1112}
Mykyta Karpin70cd3332019-09-16 18:31:03 +03001113
1114/**
1115 * Checks whether string is semver complaint version
1116 * @param string version
1117*/
1118
1119def isSemVer(version){
1120 // Official regex for Semver2 (https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string)
1121 String semVerRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
1122 return version ==~ semVerRegex
azvyagintsev898e1742020-05-12 12:40:44 +03001123}
azvyagintsevd987eb62020-06-19 15:19:05 +03001124
1125def readYaml2(LinkedHashMap kwargs) {
1126 /**
1127 * readYaml wrapper to workaround case when initial file contains
1128 * yaml data in text field so we need parse it twice.
1129 * * @param file String of filename to read
1130 * * @param text String to be read as Yaml
1131 * Parameters are mutually exclusive
1132 */
1133 if ((kwargs.get('file') && kwargs.get('text'))) {
1134 error('readYaml2 function not able to cover both ["file","text"] opts in same time ')
1135 }
1136 if (kwargs.get('file')) {
1137 data = readYaml(file: kwargs['file'])
1138 if (data instanceof String) {
1139 return readYaml(text: data)
1140 }
1141 } else if (kwargs.get('text')) {
1142 return readYaml(text: kwargs['text'])
1143 }
1144}