blob: a85196c449f8f742fe02ce46467d1d2fbb6125b5 [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
Alexandr Lovtsov2b668e52020-12-16 15:58:24 +03006import groovy.time.TimeCategory
7
Jakub Josefbceaa322017-06-13 18:28:27 +02008import com.cloudbees.groovy.cps.NonCPS
Jakub Josefb7ab8472017-04-05 14:56:53 +02009import groovy.json.JsonSlurperClassic
azvyagintsev6bda9422018-08-20 11:57:05 +030010
Sergey Kolekonov622c0132024-05-28 13:51:53 +050011import java.lang.module.ModuleDescriptor.Version
12
Dmitry Teselkin4badf832020-05-27 17:48:36 +030013import org.jenkinsci.plugins.workflow.cps.EnvActionImpl
14
Jakub Josef79ecec32017-02-17 14:36:28 +010015/**
16 *
17 * Common functions
18 *
19 */
20
21/**
22 * Generate current timestamp
23 *
azvyagintsev6bda9422018-08-20 11:57:05 +030024 * @param format Defaults to yyyyMMddHHmmss
Jakub Josef79ecec32017-02-17 14:36:28 +010025 */
azvyagintsev6bda9422018-08-20 11:57:05 +030026def getDatetime(format = "yyyyMMddHHmmss") {
Jakub Josef79ecec32017-02-17 14:36:28 +010027 def now = new Date();
28 return now.format(format, TimeZone.getTimeZone('UTC'));
29}
30
31/**
Alexandr Lovtsov2b668e52020-12-16 15:58:24 +030032 * Return Duration for given datetime period with suffix
33 *
34 * @param input String in format '\d+[smhd]', to convert given number into seconds, minutes, hours
35 * and days duration respectively. For example: '7d' is for 7 days, '10m' - 10 minutes
36 * and so on. Return null if input in incorrect format
37 */
38def getDuration(String input) {
39 // Verify input format
40 if (!input.matches('[0-9]+[smhd]')) {
41 errorMsg("Incorrect input data for getDuration(): ${input}")
42 return
43 }
44 switch (input[-1]) {
45 case 's':
46 return TimeCategory.getSeconds(input[0..-2].toInteger())
47 case 'm':
48 return TimeCategory.getMinutes(input[0..-2].toInteger())
49 case 'h':
50 return TimeCategory.getHours(input[0..-2].toInteger())
51 case 'd':
52 return TimeCategory.getDays(input[0..-2].toInteger())
53 }
54}
55
56/**
Jakub Josef79ecec32017-02-17 14:36:28 +010057 * Return workspace.
58 * Currently implemented by calling pwd so it won't return relevant result in
59 * dir context
60 */
azvyagintsev6bda9422018-08-20 11:57:05 +030061def getWorkspace(includeBuildNum = false) {
Jakub Josef79ecec32017-02-17 14:36:28 +010062 def workspace = sh script: 'pwd', returnStdout: true
63 workspace = workspace.trim()
azvyagintsev6bda9422018-08-20 11:57:05 +030064 if (includeBuildNum) {
65 if (!workspace.endsWith("/")) {
66 workspace += "/"
67 }
68 workspace += env.BUILD_NUMBER
Jakub Josefa661b8c2018-01-17 14:51:25 +010069 }
Jakub Josef79ecec32017-02-17 14:36:28 +010070 return workspace
71}
72
73/**
Dmitry Teselkind4adf972020-02-13 18:24:59 +030074 * Get absolute path via 'realink'
75 * -m, --canonicalize-missing
76 * canonicalize by following every symlink in every component of the given name recursively,
77 * without requirements on components existence
78 */
79def getAbsolutePath(String path) {
80 def absPath = sh script: "readlink -m ${path}", returnStdout: true
81 return absPath.trim()
82}
83
84/**
Filip Pytloun81c864d2017-03-21 15:19:30 +010085 * Get UID of jenkins user.
86 * Must be run from context of node
87 */
88def getJenkinsUid() {
azvyagintsev6bda9422018-08-20 11:57:05 +030089 return sh(
Filip Pytloun81c864d2017-03-21 15:19:30 +010090 script: 'id -u',
91 returnStdout: true
92 ).trim()
93}
94
95/**
96 * Get GID of jenkins user.
97 * Must be run from context of node
98 */
99def getJenkinsGid() {
azvyagintsev6bda9422018-08-20 11:57:05 +0300100 return sh(
Filip Pytloun81c864d2017-03-21 15:19:30 +0100101 script: 'id -g',
102 returnStdout: true
103 ).trim()
104}
105
106/**
Jakub Josefc8074db2018-01-30 13:33:20 +0100107 * Returns Jenkins user uid and gid in one list (in that order)
108 * Must be run from context of node
109 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300110def getJenkinsUserIds() {
Jakub Josefc8074db2018-01-30 13:33:20 +0100111 return sh(script: "id -u && id -g", returnStdout: true).tokenize("\n")
112}
113
114/**
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100115 *
116 * Find credentials by ID
117 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300118 * @param credsId Credentials ID
119 * @param credsType Credentials type (optional)
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100120 *
121 */
122def getCredentialsById(String credsId, String credsType = 'any') {
123 def credClasses = [ // ordered by class name
azvyagintsev6bda9422018-08-20 11:57:05 +0300124 sshKey : com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey.class,
125 cert : com.cloudbees.plugins.credentials.common.CertificateCredentials.class,
126 password : com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials.class,
127 any : com.cloudbees.plugins.credentials.impl.BaseStandardCredentials.class,
128 dockerCert: org.jenkinsci.plugins.docker.commons.credentials.DockerServerCredentials.class,
129 file : org.jenkinsci.plugins.plaincredentials.FileCredentials.class,
130 string : org.jenkinsci.plugins.plaincredentials.StringCredentials.class,
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100131 ]
132 return com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
133 credClasses[credsType],
134 jenkins.model.Jenkins.instance
azvyagintsev6bda9422018-08-20 11:57:05 +0300135 ).findAll { cred -> cred.id == credsId }[0]
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100136}
137
138/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100139 * Get credentials from store
140 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300141 * @param id Credentials name
Jakub Josef79ecec32017-02-17 14:36:28 +0100142 */
Jakub Josef3d9d9ab2017-03-14 15:09:03 +0100143def getCredentials(id, cred_type = "username_password") {
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100144 warningMsg('You are using obsolete function. Please switch to use `getCredentialsById()`')
Jakub Josef79ecec32017-02-17 14:36:28 +0100145
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100146 type_map = [
147 username_password: 'password',
azvyagintsev6bda9422018-08-20 11:57:05 +0300148 key : 'sshKey',
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100149 ]
Jakub Josef79ecec32017-02-17 14:36:28 +0100150
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100151 return getCredentialsById(id, type_map[cred_type])
Jakub Josef79ecec32017-02-17 14:36:28 +0100152}
153
154/**
155 * Abort build, wait for some time and ensure we will terminate
156 */
157def abortBuild() {
158 currentBuild.build().doStop()
159 sleep(180)
160 // just to be sure we will terminate
161 throw new InterruptedException()
162}
163
164/**
Jakub Josefbceaa322017-06-13 18:28:27 +0200165 * Print pretty-printed string representation of given item
166 * @param item item to be pretty-printed (list, map, whatever)
167 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300168def prettyPrint(item) {
Jakub Josefbceaa322017-06-13 18:28:27 +0200169 println prettify(item)
170}
171
172/**
Jakub Josefb41c8d52017-03-24 13:52:24 +0100173 * Return pretty-printed string representation of given item
174 * @param item item to be pretty-printed (list, map, whatever)
175 * @return pretty-printed string
176 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300177def prettify(item) {
Jakub Josefbceaa322017-06-13 18:28:27 +0200178 return groovy.json.JsonOutput.prettyPrint(toJson(item)).replace('\\n', System.getProperty('line.separator'))
Jakub Josefb41c8d52017-03-24 13:52:24 +0100179}
180
181/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100182 * Print informational message
183 *
184 * @param msg
185 * @param color Colorful output or not
186 */
187def infoMsg(msg, color = true) {
188 printMsg(msg, "cyan")
189}
190
191/**
Sergey Galkin7137ad02019-11-07 14:52:13 +0400192 * Print informational message
193 *
194 * @param msg
195 * @param color Colorful output or not
196 */
197def infoSensitivityMsg(msg, color = true, replacing = []) {
198 printSensitivityMsg(msg, "cyan", replacing)
199}
200
201/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100202 * Print error message
203 *
204 * @param msg
205 * @param color Colorful output or not
206 */
207def errorMsg(msg, color = true) {
208 printMsg(msg, "red")
209}
210
211/**
212 * Print success message
213 *
214 * @param msg
215 * @param color Colorful output or not
216 */
217def successMsg(msg, color = true) {
218 printMsg(msg, "green")
219}
220
221/**
222 * Print warning message
223 *
224 * @param msg
225 * @param color Colorful output or not
226 */
227def warningMsg(msg, color = true) {
Jakub Josef0e7bd632017-03-16 16:25:05 +0100228 printMsg(msg, "yellow")
Jakub Josef79ecec32017-02-17 14:36:28 +0100229}
230
231/**
Jakub Josef952ae0b2017-03-14 19:04:21 +0100232 * Print debug message, this message will show only if DEBUG global variable is present
233 * @param msg
234 * @param color Colorful output or not
235 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300236def debugMsg(msg, color = true) {
Jakub Josef9a836ac2017-04-24 12:26:02 +0200237 // if debug property exists on env, debug is enabled
azvyagintsev6bda9422018-08-20 11:57:05 +0300238 if (env.getEnvironment().containsKey('DEBUG') && env['DEBUG'] == "true") {
Jakub Josef74b34692017-03-15 12:10:57 +0100239 printMsg("[DEBUG] ${msg}", "red")
Jakub Josef952ae0b2017-03-14 19:04:21 +0100240 }
241}
242
azvyagintsevf6e77912018-09-07 15:41:09 +0300243def getColorizedString(msg, color) {
244 def colorMap = [
245 'red' : '\u001B[31m',
246 'black' : '\u001B[30m',
247 'green' : '\u001B[32m',
248 'yellow': '\u001B[33m',
249 'blue' : '\u001B[34m',
250 'purple': '\u001B[35m',
251 'cyan' : '\u001B[36m',
252 'white' : '\u001B[37m',
253 'reset' : '\u001B[0m'
254 ]
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300255
azvyagintsevf6e77912018-09-07 15:41:09 +0300256 return "${colorMap[color]}${msg}${colorMap.reset}"
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300257}
258
Jakub Josef952ae0b2017-03-14 19:04:21 +0100259/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100260 * Print message
261 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300262 * @param msg Message to be printed
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300263 * @param color Color to use for output
Jakub Josef79ecec32017-02-17 14:36:28 +0100264 */
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300265def printMsg(msg, color) {
266 print getColorizedString(msg, color)
Jakub Josef79ecec32017-02-17 14:36:28 +0100267}
268
269/**
Sergey Galkin7137ad02019-11-07 14:52:13 +0400270 * Print sensitivity message
271 *
272 * @param msg Message to be printed
273 * @param color Color to use for output
274 * @param replacing List with maps for deletion (passwords, logins, etc).
275 * The first () matching is mandatory !
276 * Example:
277 * [/ (OS_PASSWORD=)(.*?)+ /,
278 * / (password = )(.*?)+ /,
279 * / (password )(.*?) / ]
280 */
281def printSensitivityMsg(msg, color, replacing = []) {
282 for (i in replacing) {
283 msg = msg.replaceAll(i, ' $1XXXXXX ')
284 }
285 printMsg(msg, color)
286}
287
288/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100289 * Traverse directory structure and return list of files
290 *
291 * @param path Path to search
292 * @param type Type of files to search (groovy.io.FileType.FILES)
293 */
294@NonCPS
azvyagintsev6bda9422018-08-20 11:57:05 +0300295def getFiles(path, type = groovy.io.FileType.FILES) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100296 files = []
297 new File(path).eachFile(type) {
298 files[] = it
299 }
300 return files
301}
302
303/**
304 * Helper method to convert map into form of list of [key,value] to avoid
305 * unserializable exceptions
306 *
307 * @param m Map
308 */
309@NonCPS
310def entries(m) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300311 m.collect { k, v -> [k, v] }
Jakub Josef79ecec32017-02-17 14:36:28 +0100312}
313
314/**
315 * Opposite of build-in parallel, run map of steps in serial
316 *
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200317 * @param steps Map of String<name>: CPSClosure2<step> (or list of closures)
Jakub Josef79ecec32017-02-17 14:36:28 +0100318 */
319def serial(steps) {
320 stepsArray = entries(steps)
azvyagintsev6bda9422018-08-20 11:57:05 +0300321 for (i = 0; i < stepsArray.size; i++) {
Jakub Josefd31de302017-05-15 13:59:18 +0200322 def step = stepsArray[i]
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200323 def dummySteps = [:]
324 def stepKey
azvyagintsev6bda9422018-08-20 11:57:05 +0300325 if (step[1] instanceof List || step[1] instanceof Map) {
326 for (j = 0; j < step[1].size(); j++) {
327 if (step[1] instanceof List) {
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200328 stepKey = j
azvyagintsev6bda9422018-08-20 11:57:05 +0300329 } else if (step[1] instanceof Map) {
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200330 stepKey = step[1].keySet()[j]
331 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300332 dummySteps.put("step-${step[0]}-${stepKey}", step[1][stepKey])
Jakub Josefd31de302017-05-15 13:59:18 +0200333 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300334 } else {
Jakub Josefd31de302017-05-15 13:59:18 +0200335 dummySteps.put(step[0], step[1])
336 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100337 parallel dummySteps
338 }
339}
340
341/**
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200342 * Partition given list to list of small lists
343 * @param inputList input list
344 * @param partitionSize (partition size, optional, default 5)
345 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300346def partitionList(inputList, partitionSize = 5) {
347 List<List<String>> partitions = new ArrayList<>();
348 for (int i = 0; i < inputList.size(); i += partitionSize) {
349 partitions.add(new ArrayList<String>(inputList.subList(i, Math.min(i + partitionSize, inputList.size()))));
350 }
351 return partitions
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200352}
353
354/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100355 * Get password credentials from store
356 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300357 * @param id Credentials name
Jakub Josef79ecec32017-02-17 14:36:28 +0100358 */
359def getPasswordCredentials(id) {
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100360 return getCredentialsById(id, 'password')
Jakub Josef79ecec32017-02-17 14:36:28 +0100361}
362
363/**
364 * Get SSH credentials from store
365 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300366 * @param id Credentials name
Jakub Josef79ecec32017-02-17 14:36:28 +0100367 */
368def getSshCredentials(id) {
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100369 return getCredentialsById(id, 'sshKey')
Jakub Josef79ecec32017-02-17 14:36:28 +0100370}
Jakub Josef79ecec32017-02-17 14:36:28 +0100371
372/**
373 * Tests Jenkins instance for existence of plugin with given name
374 * @param pluginName plugin short name to test
375 * @return boolean result
376 */
377@NonCPS
azvyagintsev6bda9422018-08-20 11:57:05 +0300378def jenkinsHasPlugin(pluginName) {
379 return Jenkins.instance.pluginManager.plugins.collect { p -> p.shortName }.contains(pluginName)
Jakub Josef79ecec32017-02-17 14:36:28 +0100380}
381
382@NonCPS
383def _needNotification(notificatedTypes, buildStatus, jobName) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300384 if (notificatedTypes && notificatedTypes.contains("onchange")) {
385 if (jobName) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100386 def job = Jenkins.instance.getItem(jobName)
387 def numbuilds = job.builds.size()
azvyagintsev6bda9422018-08-20 11:57:05 +0300388 if (numbuilds > 0) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100389 //actual build is first for some reasons, so last finished build is second
390 def lastBuild = job.builds[1]
azvyagintsev6bda9422018-08-20 11:57:05 +0300391 if (lastBuild) {
392 if (lastBuild.result.toString().toLowerCase().equals(buildStatus)) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100393 println("Build status didn't changed since last build, not sending notifications")
394 return false;
395 }
396 }
397 }
398 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300399 } else if (!notificatedTypes.contains(buildStatus)) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100400 return false;
401 }
402 return true;
403}
404
405/**
406 * Send notification to all enabled notifications services
407 * @param buildStatus message type (success, warning, error), null means SUCCESSFUL
408 * @param msgText message text
409 * @param enabledNotifications list of enabled notification types, types: slack, hipchat, email, default empty
410 * @param notificatedTypes types of notifications will be sent, default onchange - notificate if current build result not equal last result;
411 * otherwise use - ["success","unstable","failed"]
412 * @param jobName optional job name param, if empty env.JOB_NAME will be used
Jakub Josefd0571152017-07-17 14:11:39 +0200413 * @param buildNumber build number param, if empty env.BUILD_NUM will be used
414 * @param buildUrl build url param, if empty env.BUILD_URL will be used
Jakub Josef79ecec32017-02-17 14:36:28 +0100415 * @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 +0200416 * @param mailTo mail TO param, it's mandatory for sending email notifications, this option enable mail notification
Jakub Josef79ecec32017-02-17 14:36:28 +0100417 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300418def sendNotification(buildStatus, msgText = "", enabledNotifications = [], notificatedTypes = ["onchange"], jobName = null, buildNumber = null, buildUrl = null, mailFrom = "jenkins", mailTo = null) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100419 // Default values
420 def colorName = 'blue'
421 def colorCode = '#0000FF'
422 def buildStatusParam = buildStatus != null && buildStatus != "" ? buildStatus : "SUCCESS"
423 def jobNameParam = jobName != null && jobName != "" ? jobName : env.JOB_NAME
424 def buildNumberParam = buildNumber != null && buildNumber != "" ? buildNumber : env.BUILD_NUMBER
425 def buildUrlParam = buildUrl != null && buildUrl != "" ? buildUrl : env.BUILD_URL
426 def subject = "${buildStatusParam}: Job '${jobNameParam} [${buildNumberParam}]'"
427 def summary = "${subject} (${buildUrlParam})"
428
azvyagintsev6bda9422018-08-20 11:57:05 +0300429 if (msgText != null && msgText != "") {
430 summary += "\n${msgText}"
Jakub Josef79ecec32017-02-17 14:36:28 +0100431 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300432 if (buildStatusParam.toLowerCase().equals("success")) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100433 colorCode = "#00FF00"
434 colorName = "green"
azvyagintsev6bda9422018-08-20 11:57:05 +0300435 } else if (buildStatusParam.toLowerCase().equals("unstable")) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100436 colorCode = "#FFFF00"
437 colorName = "yellow"
azvyagintsev6bda9422018-08-20 11:57:05 +0300438 } else if (buildStatusParam.toLowerCase().equals("failure")) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100439 colorCode = "#FF0000"
440 colorName = "red"
441 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300442 if (_needNotification(notificatedTypes, buildStatusParam.toLowerCase(), jobNameParam)) {
443 if (enabledNotifications.contains("slack") && jenkinsHasPlugin("slack")) {
444 try {
Jakub Josef79ecec32017-02-17 14:36:28 +0100445 slackSend color: colorCode, message: summary
azvyagintsev6bda9422018-08-20 11:57:05 +0300446 } catch (Exception e) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100447 println("Calling slack plugin failed")
448 e.printStackTrace()
449 }
450 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300451 if (enabledNotifications.contains("hipchat") && jenkinsHasPlugin("hipchat")) {
452 try {
Jakub Josef79ecec32017-02-17 14:36:28 +0100453 hipchatSend color: colorName.toUpperCase(), message: summary
azvyagintsev6bda9422018-08-20 11:57:05 +0300454 } catch (Exception e) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100455 println("Calling hipchat plugin failed")
456 e.printStackTrace()
457 }
458 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300459 if (enabledNotifications.contains("email") && mailTo != null && mailTo != "" && mailFrom != null && mailFrom != "") {
460 try {
Jakub Josef79ecec32017-02-17 14:36:28 +0100461 mail body: summary, from: mailFrom, subject: subject, to: mailTo
azvyagintsev6bda9422018-08-20 11:57:05 +0300462 } catch (Exception e) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100463 println("Sending mail plugin failed")
464 e.printStackTrace()
465 }
466 }
467 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100468}
chnyda4e5ac792017-03-14 15:24:18 +0100469
470/**
471 * Execute linux command and catch nth element
472 * @param cmd command to execute
473 * @param index index to retrieve
474 * @return index-th element
475 */
476
azvyagintsev6bda9422018-08-20 11:57:05 +0300477def cutOrDie(cmd, index) {
chnyda4e5ac792017-03-14 15:24:18 +0100478 def common = new com.mirantis.mk.Common()
479 def output
480 try {
azvyagintsev6bda9422018-08-20 11:57:05 +0300481 output = sh(script: cmd, returnStdout: true)
482 def result = output.tokenize(" ")[index]
483 return result;
chnyda4e5ac792017-03-14 15:24:18 +0100484 } catch (Exception e) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300485 common.errorMsg("Failed to execute cmd: ${cmd}\n output: ${output}")
chnyda4e5ac792017-03-14 15:24:18 +0100486 }
Filip Pytloun81c864d2017-03-21 15:19:30 +0100487}
Tomáš Kukrál767dd732017-03-23 10:38:59 +0100488
489/**
490 * Check variable contains keyword
491 * @param variable keywork is searched (contains) here
492 * @param keyword string to look for
493 * @return True if variable contains keyword (case insensitive), False if do not contains or any of input isn't a string
494 */
495
496def checkContains(variable, keyword) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300497 if (env.getEnvironment().containsKey(variable)) {
Jakub Josef7a8dea22017-03-23 19:51:32 +0100498 return env[variable] && env[variable].toLowerCase().contains(keyword.toLowerCase())
Tomáš Kukrál767dd732017-03-23 10:38:59 +0100499 } else {
Tomáš Kukrálc76c1e02017-03-23 19:06:59 +0100500 return false
Tomáš Kukrál767dd732017-03-23 10:38:59 +0100501 }
502}
Jakub Josefa877db52017-04-05 14:22:30 +0200503
504/**
505 * Parse JSON string to hashmap
506 * @param jsonString input JSON string
507 * @return created hashmap
508 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300509def parseJSON(jsonString) {
510 def m = [:]
511 def lazyMap = new JsonSlurperClassic().parseText(jsonString)
512 m.putAll(lazyMap)
513 return m
Jakub Josefa877db52017-04-05 14:22:30 +0200514}
Jakub Josefed239cd2017-05-09 15:27:33 +0200515
516/**
Vasyl Saienko79d6f3b2018-10-19 09:13:46 +0300517 *
518 * Deep merge of Map items. Merges variable number of maps in to onto.
519 * Using the following rules:
520 * - Lists are appended
521 * - Maps are updated
522 * - other object types are replaced.
523 *
524 *
525 * @param onto Map object to merge in
526 * @param overrides Map objects to merge to onto
527*/
528def mergeMaps(Map onto, Map... overrides){
529 if (!overrides){
530 return onto
531 }
532 else if (overrides.length == 1) {
533 overrides[0]?.each { k, v ->
Vasyl Saienkof92cf4f2019-07-10 12:39:25 +0300534 if (k in onto.keySet()) {
535 if (v in Map && onto[k] in Map){
536 mergeMaps((Map) onto[k], (Map) v)
537 } else if (v in List) {
538 onto[k] += v
539 } else {
540 onto[k] = v
541 }
Vasyl Saienko79d6f3b2018-10-19 09:13:46 +0300542 } else {
543 onto[k] = v
544 }
545 }
546 return onto
547 }
548 return overrides.inject(onto, { acc, override -> mergeMaps(acc, override ?: [:]) })
549}
550
551/**
Jakub Josefed239cd2017-05-09 15:27:33 +0200552 * Test pipeline input parameter existence and validity (not null and not empty string)
553 * @param paramName input parameter name (usually uppercase)
azvyagintsevc8ecdfd2018-09-11 12:47:15 +0300554 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300555def validInputParam(paramName) {
azvyagintsevc8ecdfd2018-09-11 12:47:15 +0300556 if (paramName instanceof java.lang.String) {
557 return env.getEnvironment().containsKey(paramName) && env[paramName] != null && env[paramName] != ""
558 }
559 return false
Tomáš Kukráld34fe872017-06-13 10:50:50 +0200560}
561
562/**
563 * Take list of hashmaps and count number of hashmaps with parameter equals eq
564 * @param lm list of hashmaps
565 * @param param define parameter of hashmap to read and compare
566 * @param eq desired value of hashmap parameter
567 * @return count of hashmaps meeting defined condition
568 */
569
570@NonCPS
571def countHashMapEquals(lm, param, eq) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300572 return lm.stream().filter { i -> i[param].equals(eq) }.collect(java.util.stream.Collectors.counting())
Tomáš Kukráld34fe872017-06-13 10:50:50 +0200573}
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200574
575/**
576 * Execute shell command and return stdout, stderr and status
577 *
578 * @param cmd Command to execute
579 * @return map with stdout, stderr, status keys
580 */
581
582def shCmdStatus(cmd) {
azvyagintsev386e94e2019-06-13 13:39:04 +0300583 // Set +x , to hide odd messages about temp file manipulations
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200584 def res = [:]
azvyagintsev386e94e2019-06-13 13:39:04 +0300585 def stderr = sh(script: 'set +x ; mktemp', returnStdout: true).trim()
586 def stdout = sh(script: 'set +x ; mktemp', returnStdout: true).trim()
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200587
588 try {
azvyagintsev6bda9422018-08-20 11:57:05 +0300589 def status = sh(script: "${cmd} 1>${stdout} 2>${stderr}", returnStatus: true)
azvyagintsev386e94e2019-06-13 13:39:04 +0300590 res['stderr'] = sh(script: "set +x; cat ${stderr}", returnStdout: true).trim()
591 res['stdout'] = sh(script: "set +x; cat ${stdout}", returnStdout: true).trim()
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200592 res['status'] = status
593 } finally {
azvyagintsev386e94e2019-06-13 13:39:04 +0300594 sh(script: "set +x; rm ${stderr}")
595 sh(script: "set +x; rm ${stdout}")
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200596 }
597
598 return res
599}
Richard Felkl66a242d2018-01-25 15:27:15 +0100600
Richard Felkl66a242d2018-01-25 15:27:15 +0100601/**
602 * Retry commands passed to body
603 *
Martin Polreich331f2b62019-02-08 10:16:52 +0100604 * Don't use common.retry method for retrying salt.enforceState method. Use retries parameter
605 * built-in the salt.enforceState method instead to ensure correct functionality.
606 *
Richard Felkl66a242d2018-01-25 15:27:15 +0100607 * @param times Number of retries
Jakub Josef61463c72018-02-13 16:10:56 +0100608 * @param delay Delay between retries (in seconds)
Richard Felkl66a242d2018-01-25 15:27:15 +0100609 * @param body Commands to be in retry block
610 * @return calling commands in body
azvyagintsev6bda9422018-08-20 11:57:05 +0300611 * @example retry ( 3 , 5 ) { function body }* retry{ function body }
Richard Felkl66a242d2018-01-25 15:27:15 +0100612 */
613
614def retry(int times = 5, int delay = 0, Closure body) {
615 int retries = 0
azvyagintsev6bda9422018-08-20 11:57:05 +0300616 while (retries++ < times) {
Richard Felkl66a242d2018-01-25 15:27:15 +0100617 try {
618 return body.call()
azvyagintsev6bda9422018-08-20 11:57:05 +0300619 } catch (e) {
Denis Egorenko900a3af2019-01-14 12:54:56 +0400620 errorMsg(e.toString())
Richard Felkl66a242d2018-01-25 15:27:15 +0100621 sleep(delay)
622 }
623 }
Richard Felkl66a242d2018-01-25 15:27:15 +0100624 throw new Exception("Failed after $times retries")
625}
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300626
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300627/**
628 * Wait for user input with timeout
629 *
630 * @param timeoutInSeconds Timeout
631 * @param options Options for input widget
632 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300633def waitForInputThenPass(timeoutInSeconds, options = [message: 'Ready to go?']) {
634 def userInput = true
635 try {
636 timeout(time: timeoutInSeconds, unit: 'SECONDS') {
637 userInput = input options
638 }
639 } catch (err) { // timeout reached or input false
640 def user = err.getCauses()[0].getUser()
641 if ('SYSTEM' == user.toString()) { // SYSTEM means timeout.
642 println("Timeout, proceeding")
643 } else {
644 userInput = false
645 println("Aborted by: [${user}]")
646 throw err
647 }
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300648 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300649 return userInput
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300650}
Oleksii Grudev9e1d97a2018-06-29 16:04:30 +0300651
652/**
653 * Function receives Map variable as input and sorts it
654 * by values ascending. Returns sorted Map
655 * @param _map Map variable
656 */
657@NonCPS
658def SortMapByValueAsc(_map) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300659 def sortedMap = _map.sort { it.value }
Oleksii Grudev9e1d97a2018-06-29 16:04:30 +0300660 return sortedMap
661}
azvyagintsev99d35842018-08-17 20:26:34 +0300662
663/**
Sergey Kolekonovbace0742023-05-18 22:27:21 +0600664 * Function receives ArrayList variable as input and sorts it
665 * by version number ascending. Returns sorted list
666 * @param _versions ArrayList variable
667 */
668@NonCPS
669def sortVersionsList(ArrayList versions) {
670 return versions.collectEntries {
671 [it, it.split(/\./).collect { (it =~ /([0-9]+).*/)[0][1] }*.toInteger()]
672 }.sort { a, b ->
673 [a.value, b.value].transpose().findResult { x, y -> x <=> y ?: null } ?:
674 a.value.size() <=> b.value.size() ?:
675 a.key <=> b.key
676 }.keySet()
677}
678
679/**
azvyagintsev99d35842018-08-17 20:26:34 +0300680 * Compare 'old' and 'new' dir's recursively
681 * @param diffData =' Only in new/XXX/infra: secrets.yml
682 Files old/XXX/init.yml and new/XXX/init.yml differ
683 Only in old/XXX/infra: secrets11.yml '
684 *
685 * @return
686 * - new:
687 - XXX/secrets.yml
688 - diff:
689 - XXX/init.yml
690 - removed:
691 - XXX/secrets11.yml
692
693 */
694def diffCheckMultidir(diffData) {
azvyagintsevab5637b2018-08-20 18:18:15 +0300695 common = new com.mirantis.mk.Common()
696 // Some global constants. Don't change\move them!
697 keyNew = 'new'
698 keyRemoved = 'removed'
699 keyDiff = 'diff'
700 def output = [
701 new : [],
702 removed: [],
703 diff : [],
704 ]
705 String pathSep = '/'
706 diffData.each { line ->
707 def job_file = ''
708 def job_type = ''
709 if (line.startsWith('Files old/')) {
710 job_file = new File(line.replace('Files old/', '').tokenize()[0])
711 job_type = keyDiff
712 } else if (line.startsWith('Only in new/')) {
713 // get clean normalized filepath, under new/
714 job_file = new File(line.replace('Only in new/', '').replace(': ', pathSep)).toString()
715 job_type = keyNew
716 } else if (line.startsWith('Only in old/')) {
717 // get clean normalized filepath, under old/
718 job_file = new File(line.replace('Only in old/', '').replace(': ', pathSep)).toString()
719 job_type = keyRemoved
720 } else {
721 common.warningMsg("Not parsed diff line: ${line}!")
722 }
723 if (job_file != '') {
724 output[job_type].push(job_file)
725 }
azvyagintsev99d35842018-08-17 20:26:34 +0300726 }
azvyagintsevab5637b2018-08-20 18:18:15 +0300727 return output
azvyagintsev99d35842018-08-17 20:26:34 +0300728}
729
730/**
731 * Compare 2 folder, file by file
732 * Structure should be:
733 * ${compRoot}/
734 └── diff - diff results will be save here
735 ├── new - input folder with data
736 ├── old - input folder with data
737 ├── pillar.diff - globall diff will be saved here
738 * b_url - usual env.BUILD_URL, to be add into description
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300739 * grepOpts - General grep cmdline; Could be used to pass some magic
740 * regexp into after-diff listing file(pillar.diff)
741 * Example: '-Ev infra/secrets.yml'
azvyagintsev99d35842018-08-17 20:26:34 +0300742 * return - html-based string
743 * TODO: allow to specify subdir for results?
azvyagintsev99d35842018-08-17 20:26:34 +0300744 **/
Denis Egorenko4551f372018-09-11 16:36:13 +0400745
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300746def comparePillars(compRoot, b_url, grepOpts) {
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +0000747
azvyagintsevab5637b2018-08-20 18:18:15 +0300748 // Some global constants. Don't change\move them!
749 keyNew = 'new'
750 keyRemoved = 'removed'
751 keyDiff = 'diff'
752 def diff_status = 0
753 // FIXME
754 httpWS = b_url + '/artifact/'
755 dir(compRoot) {
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300756 // If diff empty - exit 0
757 diff_status = sh(script: 'diff -q -r old/ new/ > pillar.diff',
azvyagintsevab5637b2018-08-20 18:18:15 +0300758 returnStatus: true,
759 )
azvyagintsev24d49652018-08-21 19:33:51 +0300760 }
azvyagintsevc0fe1442018-08-21 20:01:34 +0300761 // Unfortunately, diff not able to work with dir-based regexp
762 if (diff_status == 1 && grepOpts) {
763 dir(compRoot) {
764 grep_status = sh(script: """
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300765 cp -v pillar.diff pillar_orig.diff
766 grep ${grepOpts} pillar_orig.diff > pillar.diff
767 """,
azvyagintsevc0fe1442018-08-21 20:01:34 +0300768 returnStatus: true
769 )
azvyagintsevf6e77912018-09-07 15:41:09 +0300770 if (grep_status == 1) {
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +0000771 warningMsg("Grep regexp ${grepOpts} removed all diff!")
azvyagintsevc0fe1442018-08-21 20:01:34 +0300772 diff_status = 0
azvyagintsevb99f87c2018-08-21 19:43:59 +0300773 }
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300774 }
azvyagintsevc0fe1442018-08-21 20:01:34 +0300775 }
776 // Set job description
Denis Egorenko4551f372018-09-11 16:36:13 +0400777 description = ''
azvyagintsevc0fe1442018-08-21 20:01:34 +0300778 if (diff_status == 1) {
azvyagintsevab5637b2018-08-20 18:18:15 +0300779 // Analyse output file and prepare array with results
780 String data_ = readFile file: "${compRoot}/pillar.diff"
781 def diff_list = diffCheckMultidir(data_.split("\\r?\\n"))
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +0000782 infoMsg(diff_list)
azvyagintsevab5637b2018-08-20 18:18:15 +0300783 dir(compRoot) {
784 if (diff_list[keyDiff].size() > 0) {
785 if (!fileExists('diff')) {
786 sh('mkdir -p diff')
787 }
788 description += '<b>CHANGED</b><ul>'
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +0000789 infoMsg('Changed items:')
Denis Egorenko4551f372018-09-11 16:36:13 +0400790 def stepsForParallel = [:]
791 stepsForParallel.failFast = true
792 diff_list[keyDiff].each {
793 stepsForParallel.put("Differ for:${it}",
794 {
795 // We don't want to handle sub-dirs structure. So, simply make diff 'flat'
796 def item_f = it.toString().replace('/', '_')
797 description += "<li><a href=\"${httpWS}/diff/${item_f}/*view*/\">${it}</a></li>"
798 // Generate diff file
799 def diff_exit_code = sh([
800 script : "diff -U 50 old/${it} new/${it} > diff/${item_f}",
801 returnStdout: false,
802 returnStatus: true,
803 ])
804 // catch normal errors, diff should always return 1
805 if (diff_exit_code != 1) {
806 error 'Error with diff file generation'
807 }
808 })
azvyagintsevab5637b2018-08-20 18:18:15 +0300809 }
Denis Egorenko4551f372018-09-11 16:36:13 +0400810
811 parallel stepsForParallel
azvyagintsevab5637b2018-08-20 18:18:15 +0300812 }
813 if (diff_list[keyNew].size() > 0) {
814 description += '<b>ADDED</b><ul>'
815 for (item in diff_list[keyNew]) {
816 description += "<li><a href=\"${httpWS}/new/${item}/*view*/\">${item}</a></li>"
817 }
818 }
819 if (diff_list[keyRemoved].size() > 0) {
820 description += '<b>DELETED</b><ul>'
821 for (item in diff_list[keyRemoved]) {
822 description += "<li><a href=\"${httpWS}/old/${item}/*view*/\">${item}</a></li>"
823 }
824 }
Denis Egorenko62120962019-03-15 11:24:32 +0400825 def cwd = sh(script: 'basename $(pwd)', returnStdout: true).trim()
826 sh "tar -cf old_${cwd}.tar.gz old/ && rm -rf old/"
827 sh "tar -cf new_${cwd}.tar.gz new/ && rm -rf new/"
azvyagintsevab5637b2018-08-20 18:18:15 +0300828 }
azvyagintsev99d35842018-08-17 20:26:34 +0300829 }
azvyagintsevab5637b2018-08-20 18:18:15 +0300830
831 if (description != '') {
832 dir(compRoot) {
833 archiveArtifacts([
834 artifacts : '**',
835 allowEmptyArchive: true,
836 ])
837 }
838 return description.toString()
839 } else {
azvyagintseva57c82a2018-09-20 12:17:24 +0300840 return '<b>No job changes</b>'
azvyagintsevab5637b2018-08-20 18:18:15 +0300841 }
azvyagintsev99d35842018-08-17 20:26:34 +0300842}
azvyagintsevab5637b2018-08-20 18:18:15 +0300843
844/**
845 * Simple function, to get basename from string.
846 * line - path-string
847 * remove_ext - string, optionl. Drop file extenstion.
848 **/
849def GetBaseName(line, remove_ext) {
850 filename = line.toString().split('/').last()
851 if (remove_ext && filename.endsWith(remove_ext.toString())) {
852 filename = filename.take(filename.lastIndexOf(remove_ext.toString()))
853 }
854 return filename
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300855}
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300856
857/**
azvyagintsevf6e77912018-09-07 15:41:09 +0300858 * Return colored string of specific stage in stageMap
859 *
860 * @param stageMap LinkedHashMap object.
861 * @param stageName The name of current stage we are going to execute.
862 * @param color Text color
863 * */
864def getColoredStageView(stageMap, stageName, color) {
865 def stage = stageMap[stageName]
866 def banner = []
867 def currentStageIndex = new ArrayList<String>(stageMap.keySet()).indexOf(stageName)
868 def numberOfStages = stageMap.keySet().size() - 1
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300869
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300870 banner.add(getColorizedString(
azvyagintsevf6e77912018-09-07 15:41:09 +0300871 "=========== Stage ${currentStageIndex}/${numberOfStages}: ${stageName} ===========", color))
872 for (stage_item in stage.keySet()) {
873 banner.add(getColorizedString(
874 "${stage_item}: ${stage[stage_item]}", color))
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300875 }
azvyagintsevf6e77912018-09-07 15:41:09 +0300876 banner.add('\n')
877
878 return banner
879}
880
881/**
882 * Pring stageMap to console with specified color
883 *
884 * @param stageMap LinkedHashMap object with stages information.
885 * @param currentStage The name of current stage we are going to execute.
886 *
887 * */
888def printCurrentStage(stageMap, currentStage) {
889 print getColoredStageView(stageMap, currentStage, "cyan").join('\n')
890}
891
892/**
893 * Pring stageMap to console with specified color
894 *
895 * @param stageMap LinkedHashMap object.
896 * @param baseColor Text color (default white)
897 * */
898def printStageMap(stageMap, baseColor = "white") {
899 def banner = []
900 def index = 0
901 for (stage_name in stageMap.keySet()) {
902 banner.addAll(getColoredStageView(stageMap, stage_name, baseColor))
903 }
904 print banner.join('\n')
905}
906
907/**
908 * Wrap provided code in stage, and do interactive retires if needed.
909 *
910 * @param stageMap LinkedHashMap object with stages information.
911 * @param currentStage The name of current stage we are going to execute.
912 * @param target Target host to execute stage on.
913 * @param interactive Boolean flag to specify if interaction with user is enabled.
914 * @param body Command to be in stage block.
915 * */
916def stageWrapper(stageMap, currentStage, target, interactive = true, Closure body) {
917 def common = new com.mirantis.mk.Common()
918 def banner = []
919
920 printCurrentStage(stageMap, currentStage)
921
922 stage(currentStage) {
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300923 if (interactive){
azvyagintsevf6e77912018-09-07 15:41:09 +0300924 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 +0300925 }
926 try {
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300927 stageMap[currentStage]['Status'] = "SUCCESS"
Victor Ryzhenkin49d67812019-01-09 15:28:21 +0400928 return body.call()
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300929 } catch (Exception err) {
930 def msg = "Stage ${currentStage} failed with the following exception:\n${err}"
931 print getColorizedString(msg, "yellow")
932 common.errorMsg(err)
933 if (interactive) {
934 input message: getColorizedString("Please make sure problem is fixed to proceed with retry. Ready to proceed?", "yellow")
935 stageMap[currentStage]['Status'] = "RETRYING"
936 stageWrapper(stageMap, currentStage, target, interactive, body)
937 } else {
938 error(msg)
azvyagintsevf6e77912018-09-07 15:41:09 +0300939 }
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300940 }
azvyagintsevf6e77912018-09-07 15:41:09 +0300941 }
942}
943
944/**
945 * Ugly transition solution for internal tests.
946 * 1) Check input => transform to static result, based on runtime and input
947 * 2) Check remote-binary repo for exact resource
azvyagintsevf6fce3d2018-12-28 15:30:33 +0200948 * Return: changes each linux_system_* cto false, in case broken url in some of them
949 */
azvyagintsevf6e77912018-09-07 15:41:09 +0300950
951def checkRemoteBinary(LinkedHashMap config, List extraScmExtensions = []) {
952 def common = new com.mirantis.mk.Common()
azvyagintsevf6fce3d2018-12-28 15:30:33 +0200953 def res = [:]
azvyagintsevf6e77912018-09-07 15:41:09 +0300954 res['MirrorRoot'] = config.get('globalMirrorRoot', env["BIN_MIRROR_ROOT"] ? env["BIN_MIRROR_ROOT"] : "http://mirror.mirantis.com/")
955 // Reclass-like format's. To make life eazy!
azvyagintsev603d95b2018-11-09 15:37:10 +0200956 res['mcp_version'] = config.get('mcp_version', env["BIN_APT_MCP_VERSION"] ? env["BIN_APT_MCP_VERSION"] : 'nightly')
azvyagintsevf6fce3d2018-12-28 15:30:33 +0200957 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']}/")
958 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/")
959 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 +0300960
961 if (config.get('verify', true)) {
azvyagintsevf6fce3d2018-12-28 15:30:33 +0200962 res.each { key, val ->
963 if (key.toString().startsWith('linux_system_repo')) {
964 def MirrorRootStatus = sh(script: "wget --auth-no-challenge --spider ${val} 2>/dev/null", returnStatus: true)
965 if (MirrorRootStatus != 0) {
966 common.warningMsg("Resource: '${key}' at '${val}' not exist!")
967 res[key] = false
968 }
969 }
azvyagintsevf6e77912018-09-07 15:41:09 +0300970 }
971 }
972 return res
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300973}
Denis Egorenkoeded0d42018-09-26 13:25:49 +0400974
975/**
976 * Workaround to update env properties, like GERRIT_* vars,
977 * which should be passed from upstream job to downstream.
978 * Will not fail entire job in case any issues.
979 * @param envVar - EnvActionImpl env job
980 * @param extraVars - Multiline YAML text with extra vars
981 */
982def mergeEnv(envVar, extraVars) {
983 try {
984 def extraParams = readYaml text: extraVars
985 for(String key in extraParams.keySet()) {
986 envVar[key] = extraParams[key]
Dmitry Teselkin4badf832020-05-27 17:48:36 +0300987 println("INFO: Parameter ${key} is updated from EXTRA vars.")
Denis Egorenkoeded0d42018-09-26 13:25:49 +0400988 }
989 } catch (Exception e) {
Dmitry Teselkin4badf832020-05-27 17:48:36 +0300990 println("ERR: Can't update env parameteres, because: ${e.toString()}")
Denis Egorenkoeded0d42018-09-26 13:25:49 +0400991 }
992}
Denis Egorenko599bd632018-09-28 15:24:37 +0400993
azvyagintsev898e1742020-05-12 12:40:44 +0300994def setMapDefaults(Object base, Object defaults, Boolean recursive = false) {
995/**
996 * Function to update dict with params, if its not set yet
997 * Will not fail entire job in case any issues.
998 * Those function will not overwrite current options if already passed.
999 * @param base - dict
1000 * @param defaults - dict
1001 */
1002 if (base instanceof Map && defaults instanceof Map) {
1003 defaults.inject(base) { result, key, value ->
1004 if (result.containsKey(key)) {
1005 setMapDefaults(result[key], value, recursive = true)
1006 } else {
1007 result.put(key, value)
1008 }
1009 return result
1010 }
1011 } else if (!recursive) {
1012 echo("Can't update map parameters, wrong input data, skipping")
1013 }
1014}
1015
Dmitry Teselkin4badf832020-05-27 17:48:36 +03001016def setEnvDefaults(Object envVar, Object defaults) {
1017 /**
1018 * Function to set default values of an environment variables object
1019 * at runtime(patches existing 'env' instance).
1020 * @param env - instance of either EnvActionImpl
1021 * @param defaults - Map with default values
1022 * Example: setEnvDefaults(env, ['ENV_NAME': 'newENV_NAME', 'newvar': 'newval'])
1023 * */
1024
1025 if (!(envVar instanceof EnvActionImpl)) {
1026 error("setEnvDefaults 'env' is not an instance of EnvActionImpl")
1027 } else if (!(defaults instanceof Map)) {
1028 error("setEnvDefaults 'defaults' is not a Map")
1029 }
1030 defaults.each { key, value ->
1031 if (envVar.getEnvironment().containsKey(key)) {
1032 println("INFO:setEnvDefaults env variable ${key} already exist, not overwriting")
1033 } else {
1034 envVar[key] = value
1035 println("INFO:setEnvDefaults env variable ${key} has been added")
1036 }
1037 }
1038}
azvyagintsev898e1742020-05-12 12:40:44 +03001039
azvyagintsevc18e4732023-10-18 21:26:54 +03001040@NonCPS
1041def getEnvAsMap() {
1042 /** return env, EnvActionImpl, as usual map
1043 *
1044 */
1045 def envVars = [:]
1046 env.getEnvironment().each { k, v ->
1047 envVars[k.toString()] = v.toString()
1048 }
1049 return envVars
1050}
1051
1052@NonCPS
1053def simpleTemplate(String ptrDescriptionTemplate) {
1054 /** Render simplest template from string, using current env variables
1055 *
1056 */
1057 def engine = new groovy.text.GStringTemplateEngine()
1058 def envVars = getEnvAsMap()
1059 String result = ''
1060 try {
1061 // withDefault required to correctly set\ignore unknown defaults
1062 result = engine.createTemplate(ptrDescriptionTemplate).make(envVars.withDefault { key -> "ERROR: ${key} env variable not found" }).toString()
1063 } catch (e) {
1064 errorMsg("Failed genarate template:${ptrDescriptionTemplate}, using env vars,error:\n" + e.toString())
1065 }
1066 return result
1067}
1068
Denis Egorenko599bd632018-09-28 15:24:37 +04001069/**
1070 * Wrapper around parallel pipeline function
1071 * with ability to restrict number of parallel threads
1072 * running simultaneously
1073 *
1074 * @param branches - Map with Clousers to be executed
1075 * @param maxParallelJob - Integer number of parallel threads allowed
1076 * to run simultaneously
1077 */
1078def runParallel(branches, maxParallelJob = 10) {
1079 def runningSteps = 0
1080 branches.each { branchName, branchBody ->
1081 if (branchBody instanceof Closure) {
1082 branches[branchName] = {
1083 while (!(runningSteps < maxParallelJob)) {
1084 continue
1085 }
1086 runningSteps += 1
1087 branchBody.call()
1088 runningSteps -= 1
1089 }
1090 }
1091 }
1092 if (branches) {
1093 parallel branches
1094 }
1095}
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001096
1097/**
1098 * Ugly processing basic funcs with /etc/apt
Denis Egorenko5cea1412018-10-18 16:40:11 +04001099 * @param repoConfig YAML text or Map
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001100 * Example :
Denis Egorenko5cea1412018-10-18 16:40:11 +04001101 repoConfig = '''
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001102 ---
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001103 aprConfD: |-
Denis Egorenkoe02a1b22018-10-19 17:47:53 +04001104 APT::Get::AllowUnauthenticated 'true';
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001105 repo:
Denis Egorenkoe02a1b22018-10-19 17:47:53 +04001106 mcp_saltstack:
1107 source: "deb [arch=amd64] http://mirror.mirantis.com/nightly/saltstack-2017.7/xenial xenial main"
1108 pin:
1109 - package: "libsodium18"
1110 pin: "release o=SaltStack"
1111 priority: 50
1112 - package: "*"
1113 pin: "release o=SaltStack"
1114 priority: "1100"
1115 repo_key: "http://mirror.mirantis.com/public.gpg"
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001116 '''
1117 *
1118 */
1119
Denis Egorenko5cea1412018-10-18 16:40:11 +04001120def debianExtraRepos(repoConfig) {
1121 def config = null
1122 if (repoConfig instanceof Map) {
1123 config = repoConfig
1124 } else {
1125 config = readYaml text: repoConfig
1126 }
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001127 if (config.get('repo', false)) {
1128 for (String repo in config['repo'].keySet()) {
Denis Egorenko395aa212018-10-11 15:11:28 +04001129 source = config['repo'][repo]['source']
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001130 warningMsg("Write ${source} > /etc/apt/sources.list.d/${repo}.list")
1131 sh("echo '${source}' > /etc/apt/sources.list.d/${repo}.list")
Denis Egorenko395aa212018-10-11 15:11:28 +04001132 if (config['repo'][repo].containsKey('repo_key')) {
1133 key = config['repo'][repo]['repo_key']
1134 sh("wget -O - '${key}' | apt-key add -")
1135 }
Denis Egorenkoe02a1b22018-10-19 17:47:53 +04001136 if (config['repo'][repo]['pin']) {
1137 def repoPins = []
1138 for (Map pin in config['repo'][repo]['pin']) {
1139 repoPins.add("Package: ${pin['package']}")
1140 repoPins.add("Pin: ${pin['pin']}")
1141 repoPins.add("Pin-Priority: ${pin['priority']}")
Denis Egorenko1c93d122018-11-02 12:14:05 +04001142 // additional empty line between pins
1143 repoPins.add('\n')
Denis Egorenkoe02a1b22018-10-19 17:47:53 +04001144 }
1145 if (repoPins) {
1146 repoPins.add(0, "### Extra ${repo} repo pin start ###")
1147 repoPins.add("### Extra ${repo} repo pin end ###")
1148 repoPinning = repoPins.join('\n')
1149 warningMsg("Adding pinning \n${repoPinning}\n => /etc/apt/preferences.d/${repo}")
1150 sh("echo '${repoPinning}' > /etc/apt/preferences.d/${repo}")
1151 }
1152 }
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001153 }
1154 }
1155 if (config.get('aprConfD', false)) {
1156 for (String pref in config['aprConfD'].tokenize('\n')) {
1157 warningMsg("Adding ${pref} => /etc/apt/apt.conf.d/99setupAndTestNode")
1158 sh("echo '${pref}' >> /etc/apt/apt.conf.d/99setupAndTestNode")
1159 }
1160 sh('cat /etc/apt/apt.conf.d/99setupAndTestNode')
1161 }
1162}
Denis Egorenkoeaf78db2019-02-06 17:01:38 +04001163
1164/**
1165 * Parse date from string
1166 * @param String date - date to parse
1167 * @param String format - date format in provided date string value
1168 *
1169 * return new Date() object
1170 */
1171Date parseDate(String date, String format) {
1172 return Date.parse(format, date)
1173}
Denis Egorenko815758d2019-07-08 15:54:08 +04001174
1175/**
1176 * Generate Random Hash string
1177 * @param n Hash length
1178 * @param pool Pool to use for hash generation
1179*/
1180def generateRandomHashString(int n, ArrayList pool = []) {
1181 if (!pool) {
1182 pool = ['a'..'z','A'..'Z',0..9,'_','+','='].flatten()
1183 }
1184 Random rand = new Random(System.currentTimeMillis())
1185 return (1..n).collect { pool[rand.nextInt(pool.size())] }.join()
1186}
Mykyta Karpin70cd3332019-09-16 18:31:03 +03001187
1188/**
1189 * Checks whether string is semver complaint version
1190 * @param string version
1191*/
1192
1193def isSemVer(version){
1194 // Official regex for Semver2 (https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string)
1195 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-]+)*))?$/
1196 return version ==~ semVerRegex
azvyagintsev898e1742020-05-12 12:40:44 +03001197}
azvyagintsevd987eb62020-06-19 15:19:05 +03001198
Sergey Kolekonov622c0132024-05-28 13:51:53 +05001199
1200/**
1201 * Compare versions. Returns true if ver1 >= ver2, false if ver1 < ver2
1202 * @param string ver1
1203 * @param string ver2
1204 */
1205def isVerGreaterOrEqual (String ver1, String ver2) {
1206 Version v1 = Version.parse(ver1)
1207 Version v2 = Version.parse(ver2)
1208 return v1 >= v2
1209}
1210
Sergey Kolekonove9175ff2024-07-17 13:06:55 +05001211/**
1212 * Sort array of versions. Returns sorted array (ascending)
1213 * @param ArrayList versions
1214 */
1215def sortVersions (ArrayList versions) {
1216 for (int i = 0; i < versions.size(); i++) {
1217 for (int j = 0; j < versions.size() - i - 1; j++) {
1218 if (isVerGreaterOrEqual(versions[j], versions[j + 1])) {
1219 def temp = versions[j]
1220 versions[j] = versions[j + 1]
1221 versions[j + 1] = temp
1222 }
1223 }
1224 }
1225
1226 return versions
1227}
1228
azvyagintsevd987eb62020-06-19 15:19:05 +03001229def readYaml2(LinkedHashMap kwargs) {
1230 /**
1231 * readYaml wrapper to workaround case when initial file contains
1232 * yaml data in text field so we need parse it twice.
1233 * * @param file String of filename to read
1234 * * @param text String to be read as Yaml
1235 * Parameters are mutually exclusive
1236 */
1237 if ((kwargs.get('file') && kwargs.get('text'))) {
1238 error('readYaml2 function not able to cover both ["file","text"] opts in same time ')
1239 }
1240 if (kwargs.get('file')) {
1241 data = readYaml(file: kwargs['file'])
1242 if (data instanceof String) {
1243 return readYaml(text: data)
1244 }
azvyagintsev255cadd2020-06-22 14:50:33 +03001245 return data
azvyagintsevd987eb62020-06-19 15:19:05 +03001246 } else if (kwargs.get('text')) {
1247 return readYaml(text: kwargs['text'])
1248 }
azvyagintsev255cadd2020-06-22 14:50:33 +03001249}
Dmitry Teselkinbd57dee2020-09-23 13:05:59 +03001250
1251/**
1252 * withTempDir runs a block of code inside a new temporary directory.
1253 * This temp dir will be removed when finished.
1254 * @param: closure Closure - code block to be executed in a tmp directory
1255 *
1256 * Example:
1257 *
1258 * withTempDir {
1259 * sh "pwd"
1260 * }
1261 *
1262 **/
1263void withTempDir(Closure closure) {
1264 dir(pwd(tmp: true)) {
1265 try {
1266 closure()
1267 } finally {
1268 deleteDir()
1269 }
1270 }
1271}
Alexandr Lovtsov9c1c93a2021-02-23 14:54:37 +03001272
1273/**
1274 * Save metadata about results in yaml formatted file
1275 * @param success (boolean) answer the question "was it success or not?"
1276 * @param code (string) some generalized code to determine error
1277 * @param msg (string) user-friendly message about what's happend (error?)
1278 * @param extraMeta (map) additional data can be added to resulted yaml file trhoug this map
1279 * @param dstFile (string) name of yaml file to store the data
1280*/
1281def saveMetaResult(Boolean success, String code, String msg, Map extraMeta = [:], String dstFile = 'result.yaml') {
1282 Map result = extraMeta.clone()
1283 result.putAll([
1284 'success': success,
1285 'code': code,
1286 'message': msg,
1287 ])
1288 writeYaml file: dstFile, data: result, overwrite: true
1289 archiveArtifacts artifacts: dstFile
1290}