blob: 1507b4418159c8ac698519ce5e36ac5412c86b99 [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
Dmitry Teselkin4badf832020-05-27 17:48:36 +030011import org.jenkinsci.plugins.workflow.cps.EnvActionImpl
12
Jakub Josef79ecec32017-02-17 14:36:28 +010013/**
14 *
15 * Common functions
16 *
17 */
18
19/**
20 * Generate current timestamp
21 *
azvyagintsev6bda9422018-08-20 11:57:05 +030022 * @param format Defaults to yyyyMMddHHmmss
Jakub Josef79ecec32017-02-17 14:36:28 +010023 */
azvyagintsev6bda9422018-08-20 11:57:05 +030024def getDatetime(format = "yyyyMMddHHmmss") {
Jakub Josef79ecec32017-02-17 14:36:28 +010025 def now = new Date();
26 return now.format(format, TimeZone.getTimeZone('UTC'));
27}
28
29/**
Alexandr Lovtsov2b668e52020-12-16 15:58:24 +030030 * Return Duration for given datetime period with suffix
31 *
32 * @param input String in format '\d+[smhd]', to convert given number into seconds, minutes, hours
33 * and days duration respectively. For example: '7d' is for 7 days, '10m' - 10 minutes
34 * and so on. Return null if input in incorrect format
35 */
36def getDuration(String input) {
37 // Verify input format
38 if (!input.matches('[0-9]+[smhd]')) {
39 errorMsg("Incorrect input data for getDuration(): ${input}")
40 return
41 }
42 switch (input[-1]) {
43 case 's':
44 return TimeCategory.getSeconds(input[0..-2].toInteger())
45 case 'm':
46 return TimeCategory.getMinutes(input[0..-2].toInteger())
47 case 'h':
48 return TimeCategory.getHours(input[0..-2].toInteger())
49 case 'd':
50 return TimeCategory.getDays(input[0..-2].toInteger())
51 }
52}
53
54/**
Jakub Josef79ecec32017-02-17 14:36:28 +010055 * Return workspace.
56 * Currently implemented by calling pwd so it won't return relevant result in
57 * dir context
58 */
azvyagintsev6bda9422018-08-20 11:57:05 +030059def getWorkspace(includeBuildNum = false) {
Jakub Josef79ecec32017-02-17 14:36:28 +010060 def workspace = sh script: 'pwd', returnStdout: true
61 workspace = workspace.trim()
azvyagintsev6bda9422018-08-20 11:57:05 +030062 if (includeBuildNum) {
63 if (!workspace.endsWith("/")) {
64 workspace += "/"
65 }
66 workspace += env.BUILD_NUMBER
Jakub Josefa661b8c2018-01-17 14:51:25 +010067 }
Jakub Josef79ecec32017-02-17 14:36:28 +010068 return workspace
69}
70
71/**
Dmitry Teselkind4adf972020-02-13 18:24:59 +030072 * Get absolute path via 'realink'
73 * -m, --canonicalize-missing
74 * canonicalize by following every symlink in every component of the given name recursively,
75 * without requirements on components existence
76 */
77def getAbsolutePath(String path) {
78 def absPath = sh script: "readlink -m ${path}", returnStdout: true
79 return absPath.trim()
80}
81
82/**
Filip Pytloun81c864d2017-03-21 15:19:30 +010083 * Get UID of jenkins user.
84 * Must be run from context of node
85 */
86def getJenkinsUid() {
azvyagintsev6bda9422018-08-20 11:57:05 +030087 return sh(
Filip Pytloun81c864d2017-03-21 15:19:30 +010088 script: 'id -u',
89 returnStdout: true
90 ).trim()
91}
92
93/**
94 * Get GID of jenkins user.
95 * Must be run from context of node
96 */
97def getJenkinsGid() {
azvyagintsev6bda9422018-08-20 11:57:05 +030098 return sh(
Filip Pytloun81c864d2017-03-21 15:19:30 +010099 script: 'id -g',
100 returnStdout: true
101 ).trim()
102}
103
104/**
Jakub Josefc8074db2018-01-30 13:33:20 +0100105 * Returns Jenkins user uid and gid in one list (in that order)
106 * Must be run from context of node
107 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300108def getJenkinsUserIds() {
Jakub Josefc8074db2018-01-30 13:33:20 +0100109 return sh(script: "id -u && id -g", returnStdout: true).tokenize("\n")
110}
111
112/**
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100113 *
114 * Find credentials by ID
115 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300116 * @param credsId Credentials ID
117 * @param credsType Credentials type (optional)
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100118 *
119 */
120def getCredentialsById(String credsId, String credsType = 'any') {
121 def credClasses = [ // ordered by class name
azvyagintsev6bda9422018-08-20 11:57:05 +0300122 sshKey : com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey.class,
123 cert : com.cloudbees.plugins.credentials.common.CertificateCredentials.class,
124 password : com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials.class,
125 any : com.cloudbees.plugins.credentials.impl.BaseStandardCredentials.class,
126 dockerCert: org.jenkinsci.plugins.docker.commons.credentials.DockerServerCredentials.class,
127 file : org.jenkinsci.plugins.plaincredentials.FileCredentials.class,
128 string : org.jenkinsci.plugins.plaincredentials.StringCredentials.class,
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100129 ]
130 return com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
131 credClasses[credsType],
132 jenkins.model.Jenkins.instance
azvyagintsev6bda9422018-08-20 11:57:05 +0300133 ).findAll { cred -> cred.id == credsId }[0]
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100134}
135
136/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100137 * Get credentials from store
138 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300139 * @param id Credentials name
Jakub Josef79ecec32017-02-17 14:36:28 +0100140 */
Jakub Josef3d9d9ab2017-03-14 15:09:03 +0100141def getCredentials(id, cred_type = "username_password") {
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100142 warningMsg('You are using obsolete function. Please switch to use `getCredentialsById()`')
Jakub Josef79ecec32017-02-17 14:36:28 +0100143
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100144 type_map = [
145 username_password: 'password',
azvyagintsev6bda9422018-08-20 11:57:05 +0300146 key : 'sshKey',
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100147 ]
Jakub Josef79ecec32017-02-17 14:36:28 +0100148
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100149 return getCredentialsById(id, type_map[cred_type])
Jakub Josef79ecec32017-02-17 14:36:28 +0100150}
151
152/**
153 * Abort build, wait for some time and ensure we will terminate
154 */
155def abortBuild() {
156 currentBuild.build().doStop()
157 sleep(180)
158 // just to be sure we will terminate
159 throw new InterruptedException()
160}
161
162/**
Jakub Josefbceaa322017-06-13 18:28:27 +0200163 * Print pretty-printed string representation of given item
164 * @param item item to be pretty-printed (list, map, whatever)
165 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300166def prettyPrint(item) {
Jakub Josefbceaa322017-06-13 18:28:27 +0200167 println prettify(item)
168}
169
170/**
Jakub Josefb41c8d52017-03-24 13:52:24 +0100171 * Return pretty-printed string representation of given item
172 * @param item item to be pretty-printed (list, map, whatever)
173 * @return pretty-printed string
174 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300175def prettify(item) {
Jakub Josefbceaa322017-06-13 18:28:27 +0200176 return groovy.json.JsonOutput.prettyPrint(toJson(item)).replace('\\n', System.getProperty('line.separator'))
Jakub Josefb41c8d52017-03-24 13:52:24 +0100177}
178
179/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100180 * Print informational message
181 *
182 * @param msg
183 * @param color Colorful output or not
184 */
185def infoMsg(msg, color = true) {
186 printMsg(msg, "cyan")
187}
188
189/**
Sergey Galkin7137ad02019-11-07 14:52:13 +0400190 * Print informational message
191 *
192 * @param msg
193 * @param color Colorful output or not
194 */
195def infoSensitivityMsg(msg, color = true, replacing = []) {
196 printSensitivityMsg(msg, "cyan", replacing)
197}
198
199/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100200 * Print error message
201 *
202 * @param msg
203 * @param color Colorful output or not
204 */
205def errorMsg(msg, color = true) {
206 printMsg(msg, "red")
207}
208
209/**
210 * Print success message
211 *
212 * @param msg
213 * @param color Colorful output or not
214 */
215def successMsg(msg, color = true) {
216 printMsg(msg, "green")
217}
218
219/**
220 * Print warning message
221 *
222 * @param msg
223 * @param color Colorful output or not
224 */
225def warningMsg(msg, color = true) {
Jakub Josef0e7bd632017-03-16 16:25:05 +0100226 printMsg(msg, "yellow")
Jakub Josef79ecec32017-02-17 14:36:28 +0100227}
228
229/**
Jakub Josef952ae0b2017-03-14 19:04:21 +0100230 * Print debug message, this message will show only if DEBUG global variable is present
231 * @param msg
232 * @param color Colorful output or not
233 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300234def debugMsg(msg, color = true) {
Jakub Josef9a836ac2017-04-24 12:26:02 +0200235 // if debug property exists on env, debug is enabled
azvyagintsev6bda9422018-08-20 11:57:05 +0300236 if (env.getEnvironment().containsKey('DEBUG') && env['DEBUG'] == "true") {
Jakub Josef74b34692017-03-15 12:10:57 +0100237 printMsg("[DEBUG] ${msg}", "red")
Jakub Josef952ae0b2017-03-14 19:04:21 +0100238 }
239}
240
azvyagintsevf6e77912018-09-07 15:41:09 +0300241def getColorizedString(msg, color) {
242 def colorMap = [
243 'red' : '\u001B[31m',
244 'black' : '\u001B[30m',
245 'green' : '\u001B[32m',
246 'yellow': '\u001B[33m',
247 'blue' : '\u001B[34m',
248 'purple': '\u001B[35m',
249 'cyan' : '\u001B[36m',
250 'white' : '\u001B[37m',
251 'reset' : '\u001B[0m'
252 ]
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300253
azvyagintsevf6e77912018-09-07 15:41:09 +0300254 return "${colorMap[color]}${msg}${colorMap.reset}"
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300255}
256
Jakub Josef952ae0b2017-03-14 19:04:21 +0100257/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100258 * Print message
259 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300260 * @param msg Message to be printed
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300261 * @param color Color to use for output
Jakub Josef79ecec32017-02-17 14:36:28 +0100262 */
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300263def printMsg(msg, color) {
264 print getColorizedString(msg, color)
Jakub Josef79ecec32017-02-17 14:36:28 +0100265}
266
267/**
Sergey Galkin7137ad02019-11-07 14:52:13 +0400268 * Print sensitivity message
269 *
270 * @param msg Message to be printed
271 * @param color Color to use for output
272 * @param replacing List with maps for deletion (passwords, logins, etc).
273 * The first () matching is mandatory !
274 * Example:
275 * [/ (OS_PASSWORD=)(.*?)+ /,
276 * / (password = )(.*?)+ /,
277 * / (password )(.*?) / ]
278 */
279def printSensitivityMsg(msg, color, replacing = []) {
280 for (i in replacing) {
281 msg = msg.replaceAll(i, ' $1XXXXXX ')
282 }
283 printMsg(msg, color)
284}
285
286/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100287 * Traverse directory structure and return list of files
288 *
289 * @param path Path to search
290 * @param type Type of files to search (groovy.io.FileType.FILES)
291 */
292@NonCPS
azvyagintsev6bda9422018-08-20 11:57:05 +0300293def getFiles(path, type = groovy.io.FileType.FILES) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100294 files = []
295 new File(path).eachFile(type) {
296 files[] = it
297 }
298 return files
299}
300
301/**
302 * Helper method to convert map into form of list of [key,value] to avoid
303 * unserializable exceptions
304 *
305 * @param m Map
306 */
307@NonCPS
308def entries(m) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300309 m.collect { k, v -> [k, v] }
Jakub Josef79ecec32017-02-17 14:36:28 +0100310}
311
312/**
313 * Opposite of build-in parallel, run map of steps in serial
314 *
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200315 * @param steps Map of String<name>: CPSClosure2<step> (or list of closures)
Jakub Josef79ecec32017-02-17 14:36:28 +0100316 */
317def serial(steps) {
318 stepsArray = entries(steps)
azvyagintsev6bda9422018-08-20 11:57:05 +0300319 for (i = 0; i < stepsArray.size; i++) {
Jakub Josefd31de302017-05-15 13:59:18 +0200320 def step = stepsArray[i]
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200321 def dummySteps = [:]
322 def stepKey
azvyagintsev6bda9422018-08-20 11:57:05 +0300323 if (step[1] instanceof List || step[1] instanceof Map) {
324 for (j = 0; j < step[1].size(); j++) {
325 if (step[1] instanceof List) {
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200326 stepKey = j
azvyagintsev6bda9422018-08-20 11:57:05 +0300327 } else if (step[1] instanceof Map) {
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200328 stepKey = step[1].keySet()[j]
329 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300330 dummySteps.put("step-${step[0]}-${stepKey}", step[1][stepKey])
Jakub Josefd31de302017-05-15 13:59:18 +0200331 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300332 } else {
Jakub Josefd31de302017-05-15 13:59:18 +0200333 dummySteps.put(step[0], step[1])
334 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100335 parallel dummySteps
336 }
337}
338
339/**
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200340 * Partition given list to list of small lists
341 * @param inputList input list
342 * @param partitionSize (partition size, optional, default 5)
343 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300344def partitionList(inputList, partitionSize = 5) {
345 List<List<String>> partitions = new ArrayList<>();
346 for (int i = 0; i < inputList.size(); i += partitionSize) {
347 partitions.add(new ArrayList<String>(inputList.subList(i, Math.min(i + partitionSize, inputList.size()))));
348 }
349 return partitions
Jakub Josef7fb8bbd2017-05-15 16:02:44 +0200350}
351
352/**
Jakub Josef79ecec32017-02-17 14:36:28 +0100353 * Get password credentials from store
354 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300355 * @param id Credentials name
Jakub Josef79ecec32017-02-17 14:36:28 +0100356 */
357def getPasswordCredentials(id) {
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100358 return getCredentialsById(id, 'password')
Jakub Josef79ecec32017-02-17 14:36:28 +0100359}
360
361/**
362 * Get SSH credentials from store
363 *
azvyagintsev6bda9422018-08-20 11:57:05 +0300364 * @param id Credentials name
Jakub Josef79ecec32017-02-17 14:36:28 +0100365 */
366def getSshCredentials(id) {
Alexander Evseevbc1fea42017-12-13 10:03:03 +0100367 return getCredentialsById(id, 'sshKey')
Jakub Josef79ecec32017-02-17 14:36:28 +0100368}
Jakub Josef79ecec32017-02-17 14:36:28 +0100369
370/**
371 * Tests Jenkins instance for existence of plugin with given name
372 * @param pluginName plugin short name to test
373 * @return boolean result
374 */
375@NonCPS
azvyagintsev6bda9422018-08-20 11:57:05 +0300376def jenkinsHasPlugin(pluginName) {
377 return Jenkins.instance.pluginManager.plugins.collect { p -> p.shortName }.contains(pluginName)
Jakub Josef79ecec32017-02-17 14:36:28 +0100378}
379
380@NonCPS
381def _needNotification(notificatedTypes, buildStatus, jobName) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300382 if (notificatedTypes && notificatedTypes.contains("onchange")) {
383 if (jobName) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100384 def job = Jenkins.instance.getItem(jobName)
385 def numbuilds = job.builds.size()
azvyagintsev6bda9422018-08-20 11:57:05 +0300386 if (numbuilds > 0) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100387 //actual build is first for some reasons, so last finished build is second
388 def lastBuild = job.builds[1]
azvyagintsev6bda9422018-08-20 11:57:05 +0300389 if (lastBuild) {
390 if (lastBuild.result.toString().toLowerCase().equals(buildStatus)) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100391 println("Build status didn't changed since last build, not sending notifications")
392 return false;
393 }
394 }
395 }
396 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300397 } else if (!notificatedTypes.contains(buildStatus)) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100398 return false;
399 }
400 return true;
401}
402
403/**
404 * Send notification to all enabled notifications services
405 * @param buildStatus message type (success, warning, error), null means SUCCESSFUL
406 * @param msgText message text
407 * @param enabledNotifications list of enabled notification types, types: slack, hipchat, email, default empty
408 * @param notificatedTypes types of notifications will be sent, default onchange - notificate if current build result not equal last result;
409 * otherwise use - ["success","unstable","failed"]
410 * @param jobName optional job name param, if empty env.JOB_NAME will be used
Jakub Josefd0571152017-07-17 14:11:39 +0200411 * @param buildNumber build number param, if empty env.BUILD_NUM will be used
412 * @param buildUrl build url param, if empty env.BUILD_URL will be used
Jakub Josef79ecec32017-02-17 14:36:28 +0100413 * @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 +0200414 * @param mailTo mail TO param, it's mandatory for sending email notifications, this option enable mail notification
Jakub Josef79ecec32017-02-17 14:36:28 +0100415 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300416def sendNotification(buildStatus, msgText = "", enabledNotifications = [], notificatedTypes = ["onchange"], jobName = null, buildNumber = null, buildUrl = null, mailFrom = "jenkins", mailTo = null) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100417 // Default values
418 def colorName = 'blue'
419 def colorCode = '#0000FF'
420 def buildStatusParam = buildStatus != null && buildStatus != "" ? buildStatus : "SUCCESS"
421 def jobNameParam = jobName != null && jobName != "" ? jobName : env.JOB_NAME
422 def buildNumberParam = buildNumber != null && buildNumber != "" ? buildNumber : env.BUILD_NUMBER
423 def buildUrlParam = buildUrl != null && buildUrl != "" ? buildUrl : env.BUILD_URL
424 def subject = "${buildStatusParam}: Job '${jobNameParam} [${buildNumberParam}]'"
425 def summary = "${subject} (${buildUrlParam})"
426
azvyagintsev6bda9422018-08-20 11:57:05 +0300427 if (msgText != null && msgText != "") {
428 summary += "\n${msgText}"
Jakub Josef79ecec32017-02-17 14:36:28 +0100429 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300430 if (buildStatusParam.toLowerCase().equals("success")) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100431 colorCode = "#00FF00"
432 colorName = "green"
azvyagintsev6bda9422018-08-20 11:57:05 +0300433 } else if (buildStatusParam.toLowerCase().equals("unstable")) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100434 colorCode = "#FFFF00"
435 colorName = "yellow"
azvyagintsev6bda9422018-08-20 11:57:05 +0300436 } else if (buildStatusParam.toLowerCase().equals("failure")) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100437 colorCode = "#FF0000"
438 colorName = "red"
439 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300440 if (_needNotification(notificatedTypes, buildStatusParam.toLowerCase(), jobNameParam)) {
441 if (enabledNotifications.contains("slack") && jenkinsHasPlugin("slack")) {
442 try {
Jakub Josef79ecec32017-02-17 14:36:28 +0100443 slackSend color: colorCode, message: summary
azvyagintsev6bda9422018-08-20 11:57:05 +0300444 } catch (Exception e) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100445 println("Calling slack plugin failed")
446 e.printStackTrace()
447 }
448 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300449 if (enabledNotifications.contains("hipchat") && jenkinsHasPlugin("hipchat")) {
450 try {
Jakub Josef79ecec32017-02-17 14:36:28 +0100451 hipchatSend color: colorName.toUpperCase(), message: summary
azvyagintsev6bda9422018-08-20 11:57:05 +0300452 } catch (Exception e) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100453 println("Calling hipchat plugin failed")
454 e.printStackTrace()
455 }
456 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300457 if (enabledNotifications.contains("email") && mailTo != null && mailTo != "" && mailFrom != null && mailFrom != "") {
458 try {
Jakub Josef79ecec32017-02-17 14:36:28 +0100459 mail body: summary, from: mailFrom, subject: subject, to: mailTo
azvyagintsev6bda9422018-08-20 11:57:05 +0300460 } catch (Exception e) {
Jakub Josef79ecec32017-02-17 14:36:28 +0100461 println("Sending mail plugin failed")
462 e.printStackTrace()
463 }
464 }
465 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100466}
chnyda4e5ac792017-03-14 15:24:18 +0100467
468/**
469 * Execute linux command and catch nth element
470 * @param cmd command to execute
471 * @param index index to retrieve
472 * @return index-th element
473 */
474
azvyagintsev6bda9422018-08-20 11:57:05 +0300475def cutOrDie(cmd, index) {
chnyda4e5ac792017-03-14 15:24:18 +0100476 def common = new com.mirantis.mk.Common()
477 def output
478 try {
azvyagintsev6bda9422018-08-20 11:57:05 +0300479 output = sh(script: cmd, returnStdout: true)
480 def result = output.tokenize(" ")[index]
481 return result;
chnyda4e5ac792017-03-14 15:24:18 +0100482 } catch (Exception e) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300483 common.errorMsg("Failed to execute cmd: ${cmd}\n output: ${output}")
chnyda4e5ac792017-03-14 15:24:18 +0100484 }
Filip Pytloun81c864d2017-03-21 15:19:30 +0100485}
Tomáš Kukrál767dd732017-03-23 10:38:59 +0100486
487/**
488 * Check variable contains keyword
489 * @param variable keywork is searched (contains) here
490 * @param keyword string to look for
491 * @return True if variable contains keyword (case insensitive), False if do not contains or any of input isn't a string
492 */
493
494def checkContains(variable, keyword) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300495 if (env.getEnvironment().containsKey(variable)) {
Jakub Josef7a8dea22017-03-23 19:51:32 +0100496 return env[variable] && env[variable].toLowerCase().contains(keyword.toLowerCase())
Tomáš Kukrál767dd732017-03-23 10:38:59 +0100497 } else {
Tomáš Kukrálc76c1e02017-03-23 19:06:59 +0100498 return false
Tomáš Kukrál767dd732017-03-23 10:38:59 +0100499 }
500}
Jakub Josefa877db52017-04-05 14:22:30 +0200501
502/**
503 * Parse JSON string to hashmap
504 * @param jsonString input JSON string
505 * @return created hashmap
506 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300507def parseJSON(jsonString) {
508 def m = [:]
509 def lazyMap = new JsonSlurperClassic().parseText(jsonString)
510 m.putAll(lazyMap)
511 return m
Jakub Josefa877db52017-04-05 14:22:30 +0200512}
Jakub Josefed239cd2017-05-09 15:27:33 +0200513
514/**
Vasyl Saienko79d6f3b2018-10-19 09:13:46 +0300515 *
516 * Deep merge of Map items. Merges variable number of maps in to onto.
517 * Using the following rules:
518 * - Lists are appended
519 * - Maps are updated
520 * - other object types are replaced.
521 *
522 *
523 * @param onto Map object to merge in
524 * @param overrides Map objects to merge to onto
525*/
526def mergeMaps(Map onto, Map... overrides){
527 if (!overrides){
528 return onto
529 }
530 else if (overrides.length == 1) {
531 overrides[0]?.each { k, v ->
Vasyl Saienkof92cf4f2019-07-10 12:39:25 +0300532 if (k in onto.keySet()) {
533 if (v in Map && onto[k] in Map){
534 mergeMaps((Map) onto[k], (Map) v)
535 } else if (v in List) {
536 onto[k] += v
537 } else {
538 onto[k] = v
539 }
Vasyl Saienko79d6f3b2018-10-19 09:13:46 +0300540 } else {
541 onto[k] = v
542 }
543 }
544 return onto
545 }
546 return overrides.inject(onto, { acc, override -> mergeMaps(acc, override ?: [:]) })
547}
548
549/**
Jakub Josefed239cd2017-05-09 15:27:33 +0200550 * Test pipeline input parameter existence and validity (not null and not empty string)
551 * @param paramName input parameter name (usually uppercase)
azvyagintsevc8ecdfd2018-09-11 12:47:15 +0300552 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300553def validInputParam(paramName) {
azvyagintsevc8ecdfd2018-09-11 12:47:15 +0300554 if (paramName instanceof java.lang.String) {
555 return env.getEnvironment().containsKey(paramName) && env[paramName] != null && env[paramName] != ""
556 }
557 return false
Tomáš Kukráld34fe872017-06-13 10:50:50 +0200558}
559
560/**
561 * Take list of hashmaps and count number of hashmaps with parameter equals eq
562 * @param lm list of hashmaps
563 * @param param define parameter of hashmap to read and compare
564 * @param eq desired value of hashmap parameter
565 * @return count of hashmaps meeting defined condition
566 */
567
568@NonCPS
569def countHashMapEquals(lm, param, eq) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300570 return lm.stream().filter { i -> i[param].equals(eq) }.collect(java.util.stream.Collectors.counting())
Tomáš Kukráld34fe872017-06-13 10:50:50 +0200571}
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200572
573/**
574 * Execute shell command and return stdout, stderr and status
575 *
576 * @param cmd Command to execute
577 * @return map with stdout, stderr, status keys
578 */
579
580def shCmdStatus(cmd) {
azvyagintsev386e94e2019-06-13 13:39:04 +0300581 // Set +x , to hide odd messages about temp file manipulations
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200582 def res = [:]
azvyagintsev386e94e2019-06-13 13:39:04 +0300583 def stderr = sh(script: 'set +x ; mktemp', returnStdout: true).trim()
584 def stdout = sh(script: 'set +x ; mktemp', returnStdout: true).trim()
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200585
586 try {
azvyagintsev6bda9422018-08-20 11:57:05 +0300587 def status = sh(script: "${cmd} 1>${stdout} 2>${stderr}", returnStatus: true)
azvyagintsev386e94e2019-06-13 13:39:04 +0300588 res['stderr'] = sh(script: "set +x; cat ${stderr}", returnStdout: true).trim()
589 res['stdout'] = sh(script: "set +x; cat ${stdout}", returnStdout: true).trim()
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200590 res['status'] = status
591 } finally {
azvyagintsev386e94e2019-06-13 13:39:04 +0300592 sh(script: "set +x; rm ${stderr}")
593 sh(script: "set +x; rm ${stdout}")
Vasyl Saienkofb055b12017-11-23 18:15:23 +0200594 }
595
596 return res
597}
Richard Felkl66a242d2018-01-25 15:27:15 +0100598
Richard Felkl66a242d2018-01-25 15:27:15 +0100599/**
600 * Retry commands passed to body
601 *
Martin Polreich331f2b62019-02-08 10:16:52 +0100602 * Don't use common.retry method for retrying salt.enforceState method. Use retries parameter
603 * built-in the salt.enforceState method instead to ensure correct functionality.
604 *
Richard Felkl66a242d2018-01-25 15:27:15 +0100605 * @param times Number of retries
Jakub Josef61463c72018-02-13 16:10:56 +0100606 * @param delay Delay between retries (in seconds)
Richard Felkl66a242d2018-01-25 15:27:15 +0100607 * @param body Commands to be in retry block
608 * @return calling commands in body
azvyagintsev6bda9422018-08-20 11:57:05 +0300609 * @example retry ( 3 , 5 ) { function body }* retry{ function body }
Richard Felkl66a242d2018-01-25 15:27:15 +0100610 */
611
612def retry(int times = 5, int delay = 0, Closure body) {
613 int retries = 0
azvyagintsev6bda9422018-08-20 11:57:05 +0300614 while (retries++ < times) {
Richard Felkl66a242d2018-01-25 15:27:15 +0100615 try {
616 return body.call()
azvyagintsev6bda9422018-08-20 11:57:05 +0300617 } catch (e) {
Denis Egorenko900a3af2019-01-14 12:54:56 +0400618 errorMsg(e.toString())
Richard Felkl66a242d2018-01-25 15:27:15 +0100619 sleep(delay)
620 }
621 }
Richard Felkl66a242d2018-01-25 15:27:15 +0100622 throw new Exception("Failed after $times retries")
623}
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300624
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300625/**
626 * Wait for user input with timeout
627 *
628 * @param timeoutInSeconds Timeout
629 * @param options Options for input widget
630 */
azvyagintsev6bda9422018-08-20 11:57:05 +0300631def waitForInputThenPass(timeoutInSeconds, options = [message: 'Ready to go?']) {
632 def userInput = true
633 try {
634 timeout(time: timeoutInSeconds, unit: 'SECONDS') {
635 userInput = input options
636 }
637 } catch (err) { // timeout reached or input false
638 def user = err.getCauses()[0].getUser()
639 if ('SYSTEM' == user.toString()) { // SYSTEM means timeout.
640 println("Timeout, proceeding")
641 } else {
642 userInput = false
643 println("Aborted by: [${user}]")
644 throw err
645 }
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300646 }
azvyagintsev6bda9422018-08-20 11:57:05 +0300647 return userInput
Dmitry Pyzhov0d0d8522018-05-15 22:37:37 +0300648}
Oleksii Grudev9e1d97a2018-06-29 16:04:30 +0300649
650/**
651 * Function receives Map variable as input and sorts it
652 * by values ascending. Returns sorted Map
653 * @param _map Map variable
654 */
655@NonCPS
656def SortMapByValueAsc(_map) {
azvyagintsev6bda9422018-08-20 11:57:05 +0300657 def sortedMap = _map.sort { it.value }
Oleksii Grudev9e1d97a2018-06-29 16:04:30 +0300658 return sortedMap
659}
azvyagintsev99d35842018-08-17 20:26:34 +0300660
661/**
Sergey Kolekonovbace0742023-05-18 22:27:21 +0600662 * Function receives ArrayList variable as input and sorts it
663 * by version number ascending. Returns sorted list
664 * @param _versions ArrayList variable
665 */
666@NonCPS
667def sortVersionsList(ArrayList versions) {
668 return versions.collectEntries {
669 [it, it.split(/\./).collect { (it =~ /([0-9]+).*/)[0][1] }*.toInteger()]
670 }.sort { a, b ->
671 [a.value, b.value].transpose().findResult { x, y -> x <=> y ?: null } ?:
672 a.value.size() <=> b.value.size() ?:
673 a.key <=> b.key
674 }.keySet()
675}
676
677/**
azvyagintsev99d35842018-08-17 20:26:34 +0300678 * Compare 'old' and 'new' dir's recursively
679 * @param diffData =' Only in new/XXX/infra: secrets.yml
680 Files old/XXX/init.yml and new/XXX/init.yml differ
681 Only in old/XXX/infra: secrets11.yml '
682 *
683 * @return
684 * - new:
685 - XXX/secrets.yml
686 - diff:
687 - XXX/init.yml
688 - removed:
689 - XXX/secrets11.yml
690
691 */
692def diffCheckMultidir(diffData) {
azvyagintsevab5637b2018-08-20 18:18:15 +0300693 common = new com.mirantis.mk.Common()
694 // Some global constants. Don't change\move them!
695 keyNew = 'new'
696 keyRemoved = 'removed'
697 keyDiff = 'diff'
698 def output = [
699 new : [],
700 removed: [],
701 diff : [],
702 ]
703 String pathSep = '/'
704 diffData.each { line ->
705 def job_file = ''
706 def job_type = ''
707 if (line.startsWith('Files old/')) {
708 job_file = new File(line.replace('Files old/', '').tokenize()[0])
709 job_type = keyDiff
710 } else if (line.startsWith('Only in new/')) {
711 // get clean normalized filepath, under new/
712 job_file = new File(line.replace('Only in new/', '').replace(': ', pathSep)).toString()
713 job_type = keyNew
714 } else if (line.startsWith('Only in old/')) {
715 // get clean normalized filepath, under old/
716 job_file = new File(line.replace('Only in old/', '').replace(': ', pathSep)).toString()
717 job_type = keyRemoved
718 } else {
719 common.warningMsg("Not parsed diff line: ${line}!")
720 }
721 if (job_file != '') {
722 output[job_type].push(job_file)
723 }
azvyagintsev99d35842018-08-17 20:26:34 +0300724 }
azvyagintsevab5637b2018-08-20 18:18:15 +0300725 return output
azvyagintsev99d35842018-08-17 20:26:34 +0300726}
727
728/**
729 * Compare 2 folder, file by file
730 * Structure should be:
731 * ${compRoot}/
732 └── diff - diff results will be save here
733 ├── new - input folder with data
734 ├── old - input folder with data
735 ├── pillar.diff - globall diff will be saved here
736 * b_url - usual env.BUILD_URL, to be add into description
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300737 * grepOpts - General grep cmdline; Could be used to pass some magic
738 * regexp into after-diff listing file(pillar.diff)
739 * Example: '-Ev infra/secrets.yml'
azvyagintsev99d35842018-08-17 20:26:34 +0300740 * return - html-based string
741 * TODO: allow to specify subdir for results?
azvyagintsev99d35842018-08-17 20:26:34 +0300742 **/
Denis Egorenko4551f372018-09-11 16:36:13 +0400743
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300744def comparePillars(compRoot, b_url, grepOpts) {
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +0000745
azvyagintsevab5637b2018-08-20 18:18:15 +0300746 // Some global constants. Don't change\move them!
747 keyNew = 'new'
748 keyRemoved = 'removed'
749 keyDiff = 'diff'
750 def diff_status = 0
751 // FIXME
752 httpWS = b_url + '/artifact/'
753 dir(compRoot) {
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300754 // If diff empty - exit 0
755 diff_status = sh(script: 'diff -q -r old/ new/ > pillar.diff',
azvyagintsevab5637b2018-08-20 18:18:15 +0300756 returnStatus: true,
757 )
azvyagintsev24d49652018-08-21 19:33:51 +0300758 }
azvyagintsevc0fe1442018-08-21 20:01:34 +0300759 // Unfortunately, diff not able to work with dir-based regexp
760 if (diff_status == 1 && grepOpts) {
761 dir(compRoot) {
762 grep_status = sh(script: """
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300763 cp -v pillar.diff pillar_orig.diff
764 grep ${grepOpts} pillar_orig.diff > pillar.diff
765 """,
azvyagintsevc0fe1442018-08-21 20:01:34 +0300766 returnStatus: true
767 )
azvyagintsevf6e77912018-09-07 15:41:09 +0300768 if (grep_status == 1) {
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +0000769 warningMsg("Grep regexp ${grepOpts} removed all diff!")
azvyagintsevc0fe1442018-08-21 20:01:34 +0300770 diff_status = 0
azvyagintsevb99f87c2018-08-21 19:43:59 +0300771 }
azvyagintsev3bbcafe2018-08-20 19:36:16 +0300772 }
azvyagintsevc0fe1442018-08-21 20:01:34 +0300773 }
774 // Set job description
Denis Egorenko4551f372018-09-11 16:36:13 +0400775 description = ''
azvyagintsevc0fe1442018-08-21 20:01:34 +0300776 if (diff_status == 1) {
azvyagintsevab5637b2018-08-20 18:18:15 +0300777 // Analyse output file and prepare array with results
778 String data_ = readFile file: "${compRoot}/pillar.diff"
779 def diff_list = diffCheckMultidir(data_.split("\\r?\\n"))
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +0000780 infoMsg(diff_list)
azvyagintsevab5637b2018-08-20 18:18:15 +0300781 dir(compRoot) {
782 if (diff_list[keyDiff].size() > 0) {
783 if (!fileExists('diff')) {
784 sh('mkdir -p diff')
785 }
786 description += '<b>CHANGED</b><ul>'
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +0000787 infoMsg('Changed items:')
Denis Egorenko4551f372018-09-11 16:36:13 +0400788 def stepsForParallel = [:]
789 stepsForParallel.failFast = true
790 diff_list[keyDiff].each {
791 stepsForParallel.put("Differ for:${it}",
792 {
793 // We don't want to handle sub-dirs structure. So, simply make diff 'flat'
794 def item_f = it.toString().replace('/', '_')
795 description += "<li><a href=\"${httpWS}/diff/${item_f}/*view*/\">${it}</a></li>"
796 // Generate diff file
797 def diff_exit_code = sh([
798 script : "diff -U 50 old/${it} new/${it} > diff/${item_f}",
799 returnStdout: false,
800 returnStatus: true,
801 ])
802 // catch normal errors, diff should always return 1
803 if (diff_exit_code != 1) {
804 error 'Error with diff file generation'
805 }
806 })
azvyagintsevab5637b2018-08-20 18:18:15 +0300807 }
Denis Egorenko4551f372018-09-11 16:36:13 +0400808
809 parallel stepsForParallel
azvyagintsevab5637b2018-08-20 18:18:15 +0300810 }
811 if (diff_list[keyNew].size() > 0) {
812 description += '<b>ADDED</b><ul>'
813 for (item in diff_list[keyNew]) {
814 description += "<li><a href=\"${httpWS}/new/${item}/*view*/\">${item}</a></li>"
815 }
816 }
817 if (diff_list[keyRemoved].size() > 0) {
818 description += '<b>DELETED</b><ul>'
819 for (item in diff_list[keyRemoved]) {
820 description += "<li><a href=\"${httpWS}/old/${item}/*view*/\">${item}</a></li>"
821 }
822 }
Denis Egorenko62120962019-03-15 11:24:32 +0400823 def cwd = sh(script: 'basename $(pwd)', returnStdout: true).trim()
824 sh "tar -cf old_${cwd}.tar.gz old/ && rm -rf old/"
825 sh "tar -cf new_${cwd}.tar.gz new/ && rm -rf new/"
azvyagintsevab5637b2018-08-20 18:18:15 +0300826 }
azvyagintsev99d35842018-08-17 20:26:34 +0300827 }
azvyagintsevab5637b2018-08-20 18:18:15 +0300828
829 if (description != '') {
830 dir(compRoot) {
831 archiveArtifacts([
832 artifacts : '**',
833 allowEmptyArchive: true,
834 ])
835 }
836 return description.toString()
837 } else {
azvyagintseva57c82a2018-09-20 12:17:24 +0300838 return '<b>No job changes</b>'
azvyagintsevab5637b2018-08-20 18:18:15 +0300839 }
azvyagintsev99d35842018-08-17 20:26:34 +0300840}
azvyagintsevab5637b2018-08-20 18:18:15 +0300841
842/**
843 * Simple function, to get basename from string.
844 * line - path-string
845 * remove_ext - string, optionl. Drop file extenstion.
846 **/
847def GetBaseName(line, remove_ext) {
848 filename = line.toString().split('/').last()
849 if (remove_ext && filename.endsWith(remove_ext.toString())) {
850 filename = filename.take(filename.lastIndexOf(remove_ext.toString()))
851 }
852 return filename
Vasyl Saienko00ef98b2018-09-05 10:34:32 +0300853}
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300854
855/**
azvyagintsevf6e77912018-09-07 15:41:09 +0300856 * Return colored string of specific stage in stageMap
857 *
858 * @param stageMap LinkedHashMap object.
859 * @param stageName The name of current stage we are going to execute.
860 * @param color Text color
861 * */
862def getColoredStageView(stageMap, stageName, color) {
863 def stage = stageMap[stageName]
864 def banner = []
865 def currentStageIndex = new ArrayList<String>(stageMap.keySet()).indexOf(stageName)
866 def numberOfStages = stageMap.keySet().size() - 1
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300867
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300868 banner.add(getColorizedString(
azvyagintsevf6e77912018-09-07 15:41:09 +0300869 "=========== Stage ${currentStageIndex}/${numberOfStages}: ${stageName} ===========", color))
870 for (stage_item in stage.keySet()) {
871 banner.add(getColorizedString(
872 "${stage_item}: ${stage[stage_item]}", color))
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300873 }
azvyagintsevf6e77912018-09-07 15:41:09 +0300874 banner.add('\n')
875
876 return banner
877}
878
879/**
880 * Pring stageMap to console with specified color
881 *
882 * @param stageMap LinkedHashMap object with stages information.
883 * @param currentStage The name of current stage we are going to execute.
884 *
885 * */
886def printCurrentStage(stageMap, currentStage) {
887 print getColoredStageView(stageMap, currentStage, "cyan").join('\n')
888}
889
890/**
891 * Pring stageMap to console with specified color
892 *
893 * @param stageMap LinkedHashMap object.
894 * @param baseColor Text color (default white)
895 * */
896def printStageMap(stageMap, baseColor = "white") {
897 def banner = []
898 def index = 0
899 for (stage_name in stageMap.keySet()) {
900 banner.addAll(getColoredStageView(stageMap, stage_name, baseColor))
901 }
902 print banner.join('\n')
903}
904
905/**
906 * Wrap provided code in stage, and do interactive retires if needed.
907 *
908 * @param stageMap LinkedHashMap object with stages information.
909 * @param currentStage The name of current stage we are going to execute.
910 * @param target Target host to execute stage on.
911 * @param interactive Boolean flag to specify if interaction with user is enabled.
912 * @param body Command to be in stage block.
913 * */
914def stageWrapper(stageMap, currentStage, target, interactive = true, Closure body) {
915 def common = new com.mirantis.mk.Common()
916 def banner = []
917
918 printCurrentStage(stageMap, currentStage)
919
920 stage(currentStage) {
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300921 if (interactive){
azvyagintsevf6e77912018-09-07 15:41:09 +0300922 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 +0300923 }
924 try {
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300925 stageMap[currentStage]['Status'] = "SUCCESS"
Victor Ryzhenkin49d67812019-01-09 15:28:21 +0400926 return body.call()
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300927 } catch (Exception err) {
928 def msg = "Stage ${currentStage} failed with the following exception:\n${err}"
929 print getColorizedString(msg, "yellow")
930 common.errorMsg(err)
931 if (interactive) {
932 input message: getColorizedString("Please make sure problem is fixed to proceed with retry. Ready to proceed?", "yellow")
933 stageMap[currentStage]['Status'] = "RETRYING"
934 stageWrapper(stageMap, currentStage, target, interactive, body)
935 } else {
936 error(msg)
azvyagintsevf6e77912018-09-07 15:41:09 +0300937 }
Vasyl Saienkobc6debb2018-09-10 14:08:09 +0300938 }
azvyagintsevf6e77912018-09-07 15:41:09 +0300939 }
940}
941
942/**
943 * Ugly transition solution for internal tests.
944 * 1) Check input => transform to static result, based on runtime and input
945 * 2) Check remote-binary repo for exact resource
azvyagintsevf6fce3d2018-12-28 15:30:33 +0200946 * Return: changes each linux_system_* cto false, in case broken url in some of them
947 */
azvyagintsevf6e77912018-09-07 15:41:09 +0300948
949def checkRemoteBinary(LinkedHashMap config, List extraScmExtensions = []) {
950 def common = new com.mirantis.mk.Common()
azvyagintsevf6fce3d2018-12-28 15:30:33 +0200951 def res = [:]
azvyagintsevf6e77912018-09-07 15:41:09 +0300952 res['MirrorRoot'] = config.get('globalMirrorRoot', env["BIN_MIRROR_ROOT"] ? env["BIN_MIRROR_ROOT"] : "http://mirror.mirantis.com/")
953 // Reclass-like format's. To make life eazy!
azvyagintsev603d95b2018-11-09 15:37:10 +0200954 res['mcp_version'] = config.get('mcp_version', env["BIN_APT_MCP_VERSION"] ? env["BIN_APT_MCP_VERSION"] : 'nightly')
azvyagintsevf6fce3d2018-12-28 15:30:33 +0200955 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']}/")
956 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/")
957 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 +0300958
959 if (config.get('verify', true)) {
azvyagintsevf6fce3d2018-12-28 15:30:33 +0200960 res.each { key, val ->
961 if (key.toString().startsWith('linux_system_repo')) {
962 def MirrorRootStatus = sh(script: "wget --auth-no-challenge --spider ${val} 2>/dev/null", returnStatus: true)
963 if (MirrorRootStatus != 0) {
964 common.warningMsg("Resource: '${key}' at '${val}' not exist!")
965 res[key] = false
966 }
967 }
azvyagintsevf6e77912018-09-07 15:41:09 +0300968 }
969 }
970 return res
Vasyl Saienko723cbbc2018-09-05 11:08:52 +0300971}
Denis Egorenkoeded0d42018-09-26 13:25:49 +0400972
973/**
974 * Workaround to update env properties, like GERRIT_* vars,
975 * which should be passed from upstream job to downstream.
976 * Will not fail entire job in case any issues.
977 * @param envVar - EnvActionImpl env job
978 * @param extraVars - Multiline YAML text with extra vars
979 */
980def mergeEnv(envVar, extraVars) {
981 try {
982 def extraParams = readYaml text: extraVars
983 for(String key in extraParams.keySet()) {
984 envVar[key] = extraParams[key]
Dmitry Teselkin4badf832020-05-27 17:48:36 +0300985 println("INFO: Parameter ${key} is updated from EXTRA vars.")
Denis Egorenkoeded0d42018-09-26 13:25:49 +0400986 }
987 } catch (Exception e) {
Dmitry Teselkin4badf832020-05-27 17:48:36 +0300988 println("ERR: Can't update env parameteres, because: ${e.toString()}")
Denis Egorenkoeded0d42018-09-26 13:25:49 +0400989 }
990}
Denis Egorenko599bd632018-09-28 15:24:37 +0400991
azvyagintsev898e1742020-05-12 12:40:44 +0300992def setMapDefaults(Object base, Object defaults, Boolean recursive = false) {
993/**
994 * Function to update dict with params, if its not set yet
995 * Will not fail entire job in case any issues.
996 * Those function will not overwrite current options if already passed.
997 * @param base - dict
998 * @param defaults - dict
999 */
1000 if (base instanceof Map && defaults instanceof Map) {
1001 defaults.inject(base) { result, key, value ->
1002 if (result.containsKey(key)) {
1003 setMapDefaults(result[key], value, recursive = true)
1004 } else {
1005 result.put(key, value)
1006 }
1007 return result
1008 }
1009 } else if (!recursive) {
1010 echo("Can't update map parameters, wrong input data, skipping")
1011 }
1012}
1013
Dmitry Teselkin4badf832020-05-27 17:48:36 +03001014def setEnvDefaults(Object envVar, Object defaults) {
1015 /**
1016 * Function to set default values of an environment variables object
1017 * at runtime(patches existing 'env' instance).
1018 * @param env - instance of either EnvActionImpl
1019 * @param defaults - Map with default values
1020 * Example: setEnvDefaults(env, ['ENV_NAME': 'newENV_NAME', 'newvar': 'newval'])
1021 * */
1022
1023 if (!(envVar instanceof EnvActionImpl)) {
1024 error("setEnvDefaults 'env' is not an instance of EnvActionImpl")
1025 } else if (!(defaults instanceof Map)) {
1026 error("setEnvDefaults 'defaults' is not a Map")
1027 }
1028 defaults.each { key, value ->
1029 if (envVar.getEnvironment().containsKey(key)) {
1030 println("INFO:setEnvDefaults env variable ${key} already exist, not overwriting")
1031 } else {
1032 envVar[key] = value
1033 println("INFO:setEnvDefaults env variable ${key} has been added")
1034 }
1035 }
1036}
azvyagintsev898e1742020-05-12 12:40:44 +03001037
azvyagintsevc18e4732023-10-18 21:26:54 +03001038@NonCPS
1039def getEnvAsMap() {
1040 /** return env, EnvActionImpl, as usual map
1041 *
1042 */
1043 def envVars = [:]
1044 env.getEnvironment().each { k, v ->
1045 envVars[k.toString()] = v.toString()
1046 }
1047 return envVars
1048}
1049
1050@NonCPS
1051def simpleTemplate(String ptrDescriptionTemplate) {
1052 /** Render simplest template from string, using current env variables
1053 *
1054 */
1055 def engine = new groovy.text.GStringTemplateEngine()
1056 def envVars = getEnvAsMap()
1057 String result = ''
1058 try {
1059 // withDefault required to correctly set\ignore unknown defaults
1060 result = engine.createTemplate(ptrDescriptionTemplate).make(envVars.withDefault { key -> "ERROR: ${key} env variable not found" }).toString()
1061 } catch (e) {
1062 errorMsg("Failed genarate template:${ptrDescriptionTemplate}, using env vars,error:\n" + e.toString())
1063 }
1064 return result
1065}
1066
Denis Egorenko599bd632018-09-28 15:24:37 +04001067/**
1068 * Wrapper around parallel pipeline function
1069 * with ability to restrict number of parallel threads
1070 * running simultaneously
1071 *
1072 * @param branches - Map with Clousers to be executed
1073 * @param maxParallelJob - Integer number of parallel threads allowed
1074 * to run simultaneously
1075 */
1076def runParallel(branches, maxParallelJob = 10) {
1077 def runningSteps = 0
1078 branches.each { branchName, branchBody ->
1079 if (branchBody instanceof Closure) {
1080 branches[branchName] = {
1081 while (!(runningSteps < maxParallelJob)) {
1082 continue
1083 }
1084 runningSteps += 1
1085 branchBody.call()
1086 runningSteps -= 1
1087 }
1088 }
1089 }
1090 if (branches) {
1091 parallel branches
1092 }
1093}
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001094
1095/**
1096 * Ugly processing basic funcs with /etc/apt
Denis Egorenko5cea1412018-10-18 16:40:11 +04001097 * @param repoConfig YAML text or Map
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001098 * Example :
Denis Egorenko5cea1412018-10-18 16:40:11 +04001099 repoConfig = '''
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001100 ---
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001101 aprConfD: |-
Denis Egorenkoe02a1b22018-10-19 17:47:53 +04001102 APT::Get::AllowUnauthenticated 'true';
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001103 repo:
Denis Egorenkoe02a1b22018-10-19 17:47:53 +04001104 mcp_saltstack:
1105 source: "deb [arch=amd64] http://mirror.mirantis.com/nightly/saltstack-2017.7/xenial xenial main"
1106 pin:
1107 - package: "libsodium18"
1108 pin: "release o=SaltStack"
1109 priority: 50
1110 - package: "*"
1111 pin: "release o=SaltStack"
1112 priority: "1100"
1113 repo_key: "http://mirror.mirantis.com/public.gpg"
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001114 '''
1115 *
1116 */
1117
Denis Egorenko5cea1412018-10-18 16:40:11 +04001118def debianExtraRepos(repoConfig) {
1119 def config = null
1120 if (repoConfig instanceof Map) {
1121 config = repoConfig
1122 } else {
1123 config = readYaml text: repoConfig
1124 }
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001125 if (config.get('repo', false)) {
1126 for (String repo in config['repo'].keySet()) {
Denis Egorenko395aa212018-10-11 15:11:28 +04001127 source = config['repo'][repo]['source']
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001128 warningMsg("Write ${source} > /etc/apt/sources.list.d/${repo}.list")
1129 sh("echo '${source}' > /etc/apt/sources.list.d/${repo}.list")
Denis Egorenko395aa212018-10-11 15:11:28 +04001130 if (config['repo'][repo].containsKey('repo_key')) {
1131 key = config['repo'][repo]['repo_key']
1132 sh("wget -O - '${key}' | apt-key add -")
1133 }
Denis Egorenkoe02a1b22018-10-19 17:47:53 +04001134 if (config['repo'][repo]['pin']) {
1135 def repoPins = []
1136 for (Map pin in config['repo'][repo]['pin']) {
1137 repoPins.add("Package: ${pin['package']}")
1138 repoPins.add("Pin: ${pin['pin']}")
1139 repoPins.add("Pin-Priority: ${pin['priority']}")
Denis Egorenko1c93d122018-11-02 12:14:05 +04001140 // additional empty line between pins
1141 repoPins.add('\n')
Denis Egorenkoe02a1b22018-10-19 17:47:53 +04001142 }
1143 if (repoPins) {
1144 repoPins.add(0, "### Extra ${repo} repo pin start ###")
1145 repoPins.add("### Extra ${repo} repo pin end ###")
1146 repoPinning = repoPins.join('\n')
1147 warningMsg("Adding pinning \n${repoPinning}\n => /etc/apt/preferences.d/${repo}")
1148 sh("echo '${repoPinning}' > /etc/apt/preferences.d/${repo}")
1149 }
1150 }
Aleksey Zvyagintsevb20bd262018-10-05 15:09:06 +00001151 }
1152 }
1153 if (config.get('aprConfD', false)) {
1154 for (String pref in config['aprConfD'].tokenize('\n')) {
1155 warningMsg("Adding ${pref} => /etc/apt/apt.conf.d/99setupAndTestNode")
1156 sh("echo '${pref}' >> /etc/apt/apt.conf.d/99setupAndTestNode")
1157 }
1158 sh('cat /etc/apt/apt.conf.d/99setupAndTestNode')
1159 }
1160}
Denis Egorenkoeaf78db2019-02-06 17:01:38 +04001161
1162/**
1163 * Parse date from string
1164 * @param String date - date to parse
1165 * @param String format - date format in provided date string value
1166 *
1167 * return new Date() object
1168 */
1169Date parseDate(String date, String format) {
1170 return Date.parse(format, date)
1171}
Denis Egorenko815758d2019-07-08 15:54:08 +04001172
1173/**
1174 * Generate Random Hash string
1175 * @param n Hash length
1176 * @param pool Pool to use for hash generation
1177*/
1178def generateRandomHashString(int n, ArrayList pool = []) {
1179 if (!pool) {
1180 pool = ['a'..'z','A'..'Z',0..9,'_','+','='].flatten()
1181 }
1182 Random rand = new Random(System.currentTimeMillis())
1183 return (1..n).collect { pool[rand.nextInt(pool.size())] }.join()
1184}
Mykyta Karpin70cd3332019-09-16 18:31:03 +03001185
1186/**
1187 * Checks whether string is semver complaint version
1188 * @param string version
1189*/
1190
1191def isSemVer(version){
1192 // Official regex for Semver2 (https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string)
1193 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-]+)*))?$/
1194 return version ==~ semVerRegex
azvyagintsev898e1742020-05-12 12:40:44 +03001195}
azvyagintsevd987eb62020-06-19 15:19:05 +03001196
1197def readYaml2(LinkedHashMap kwargs) {
1198 /**
1199 * readYaml wrapper to workaround case when initial file contains
1200 * yaml data in text field so we need parse it twice.
1201 * * @param file String of filename to read
1202 * * @param text String to be read as Yaml
1203 * Parameters are mutually exclusive
1204 */
1205 if ((kwargs.get('file') && kwargs.get('text'))) {
1206 error('readYaml2 function not able to cover both ["file","text"] opts in same time ')
1207 }
1208 if (kwargs.get('file')) {
1209 data = readYaml(file: kwargs['file'])
1210 if (data instanceof String) {
1211 return readYaml(text: data)
1212 }
azvyagintsev255cadd2020-06-22 14:50:33 +03001213 return data
azvyagintsevd987eb62020-06-19 15:19:05 +03001214 } else if (kwargs.get('text')) {
1215 return readYaml(text: kwargs['text'])
1216 }
azvyagintsev255cadd2020-06-22 14:50:33 +03001217}
Dmitry Teselkinbd57dee2020-09-23 13:05:59 +03001218
1219/**
1220 * withTempDir runs a block of code inside a new temporary directory.
1221 * This temp dir will be removed when finished.
1222 * @param: closure Closure - code block to be executed in a tmp directory
1223 *
1224 * Example:
1225 *
1226 * withTempDir {
1227 * sh "pwd"
1228 * }
1229 *
1230 **/
1231void withTempDir(Closure closure) {
1232 dir(pwd(tmp: true)) {
1233 try {
1234 closure()
1235 } finally {
1236 deleteDir()
1237 }
1238 }
1239}
Alexandr Lovtsov9c1c93a2021-02-23 14:54:37 +03001240
1241/**
1242 * Save metadata about results in yaml formatted file
1243 * @param success (boolean) answer the question "was it success or not?"
1244 * @param code (string) some generalized code to determine error
1245 * @param msg (string) user-friendly message about what's happend (error?)
1246 * @param extraMeta (map) additional data can be added to resulted yaml file trhoug this map
1247 * @param dstFile (string) name of yaml file to store the data
1248*/
1249def saveMetaResult(Boolean success, String code, String msg, Map extraMeta = [:], String dstFile = 'result.yaml') {
1250 Map result = extraMeta.clone()
1251 result.putAll([
1252 'success': success,
1253 'code': code,
1254 'message': msg,
1255 ])
1256 writeYaml file: dstFile, data: result, overwrite: true
1257 archiveArtifacts artifacts: dstFile
1258}