blob: ad77e2b1a42eb694cc17544e1a71d0d21fcaafda [file] [log] [blame]
Jakub Josef79ecec32017-02-17 14:36:28 +01001package com.mirantis.mk
2
Jakub Josefb77c0812017-03-27 14:11:01 +02003import java.util.stream.Collectors
Jakub Josef79ecec32017-02-17 14:36:28 +01004/**
5 * Salt functions
6 *
7*/
8
9/**
10 * Salt connection and context parameters
11 *
12 * @param url Salt API server URL
13 * @param credentialsID ID of credentials store entry
14 */
15def connection(url, credentialsId = "salt") {
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +010016 def common = new com.mirantis.mk.Common()
Jakub Josef79ecec32017-02-17 14:36:28 +010017 params = [
18 "url": url,
19 "credentialsId": credentialsId,
20 "authToken": null,
21 "creds": common.getCredentials(credentialsId)
22 ]
23 params["authToken"] = saltLogin(params)
24
25 return params
26}
27
28/**
29 * Login to Salt API, return auth token
30 *
31 * @param master Salt connection object
32 */
33def saltLogin(master) {
Tomáš Kukrál7bec0532017-02-20 15:39:31 +010034 def http = new com.mirantis.mk.Http()
Jakub Josef79ecec32017-02-17 14:36:28 +010035 data = [
36 'username': master.creds.username,
37 'password': master.creds.password.toString(),
38 'eauth': 'pam'
39 ]
Tomáš Kukrál7bec0532017-02-20 15:39:31 +010040 authToken = http.restGet(master, '/login', data)['return'][0]['token']
Jakub Josef79ecec32017-02-17 14:36:28 +010041 return authToken
42}
43
44/**
45 * Run action using Salt API
46 *
47 * @param master Salt connection object
48 * @param client Client type
49 * @param target Target specification, eg. for compound matches by Pillar
50 * data: ['expression': 'I@openssh:server', 'type': 'compound'])
51 * @param function Function to execute (eg. "state.sls")
Jakub Josef2f25cf22017-03-28 13:34:57 +020052 * @param batch Batch param to salt (integer or string with percents)
Jakub Josef79ecec32017-02-17 14:36:28 +010053 * @param args Additional arguments to function
54 * @param kwargs Additional key-value arguments to function
55 */
56@NonCPS
57def runSaltCommand(master, client, target, function, batch = null, args = null, kwargs = null) {
iberezovskiyd4240b52017-02-20 17:18:28 +040058 def http = new com.mirantis.mk.Http()
Jakub Josef79ecec32017-02-17 14:36:28 +010059
60 data = [
61 'tgt': target.expression,
62 'fun': function,
63 'client': client,
64 'expr_form': target.type,
65 ]
66
Jakub Josef5f838212017-04-06 12:43:58 +020067 if(batch != null && ( (batch instanceof Integer && batch > 0) || (batch instanceof String && batch.contains("%")))){
Jakub Josef2f25cf22017-03-28 13:34:57 +020068 data['client']= "local_batch"
69 data['batch'] = batch
Jakub Josef79ecec32017-02-17 14:36:28 +010070 }
71
72 if (args) {
73 data['arg'] = args
74 }
75
76 if (kwargs) {
77 data['kwarg'] = kwargs
78 }
79
80 headers = [
81 'X-Auth-Token': "${master.authToken}"
82 ]
83
84 return http.sendHttpPostRequest("${master.url}/", data, headers)
85}
86
Jakub Josef5ade54c2017-03-10 16:14:01 +010087/**
88 * Return pillar for given master and target
89 * @param master Salt connection object
90 * @param target Get pillar target
91 * @param pillar pillar name (optional)
92 * @return output of salt command
93 */
Ales Komarekcec24d42017-03-08 10:25:45 +010094def getPillar(master, target, pillar = null) {
Tomáš Kukráld2589702017-03-10 16:30:46 +010095 if (pillar != null) {
Jakub Josef5ade54c2017-03-10 16:14:01 +010096 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'pillar.get', null, [pillar.replace('.', ':')])
Tomáš Kukráld2589702017-03-10 16:30:46 +010097 } else {
Jakub Josef5ade54c2017-03-10 16:14:01 +010098 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'pillar.data')
Ales Komareka3c7e502017-03-13 11:20:44 +010099 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100100}
101
Jakub Josef5ade54c2017-03-10 16:14:01 +0100102/**
103 * Return grain for given master and target
104 * @param master Salt connection object
105 * @param target Get grain target
106 * @param grain grain name (optional)
107 * @return output of salt command
108 */
Ales Komarekcec24d42017-03-08 10:25:45 +0100109def getGrain(master, target, grain = null) {
110 if(grain != null) {
Jiri Broulik852dfd52017-05-16 13:38:55 +0200111 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'grains.item', null, [grain])
Jakub Josef5ade54c2017-03-10 16:14:01 +0100112 } else {
Jiri Broulik852dfd52017-05-16 13:38:55 +0200113 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'grains.items')
Ales Komarekcec24d42017-03-08 10:25:45 +0100114 }
Ales Komarekcec24d42017-03-08 10:25:45 +0100115}
116
Jakub Josef5ade54c2017-03-10 16:14:01 +0100117/**
118 * Enforces state on given master and target
119 * @param master Salt connection object
120 * @param target State enforcing target
121 * @param state Salt state
122 * @param output print output (optional, default true)
123 * @param failOnError throw exception on salt state result:false (optional, default true)
Jakub Josef2f25cf22017-03-28 13:34:57 +0200124 * @param batch salt batch parameter integer or string with percents (optional, default null - disable batch)
Jakub Josef5ade54c2017-03-10 16:14:01 +0100125 * @return output of salt command
126 */
Jakub Josef2f25cf22017-03-28 13:34:57 +0200127def enforceState(master, target, state, output = true, failOnError = true, batch = null) {
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100128 def common = new com.mirantis.mk.Common()
Jakub Josef79ecec32017-02-17 14:36:28 +0100129 def run_states
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100130
Jakub Josef79ecec32017-02-17 14:36:28 +0100131 if (state instanceof String) {
132 run_states = state
133 } else {
134 run_states = state.join(',')
135 }
136
Tomáš Kukráldfd4b492017-03-02 12:08:50 +0100137 common.infoMsg("Enforcing state ${run_states} on ${target}")
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100138
Jakub Josef2f25cf22017-03-28 13:34:57 +0200139 def out = runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'state.sls', batch, [run_states])
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100140
Jakub Josef374beb72017-04-27 15:45:09 +0200141 checkResult(out, failOnError, output)
Jakub Josef79ecec32017-02-17 14:36:28 +0100142 return out
143}
144
Jakub Josef5ade54c2017-03-10 16:14:01 +0100145/**
146 * Run command on salt minion (salt cmd.run wrapper)
147 * @param master Salt connection object
148 * @param target Get pillar target
149 * @param cmd command
Jakub Josef053df392017-05-03 15:51:05 +0200150 * @param checkResponse test command success execution (default true)
Jakub Josef2f25cf22017-03-28 13:34:57 +0200151 * @param batch salt batch parameter integer or string with percents (optional, default null - disable batch)
Jakub Josef5ade54c2017-03-10 16:14:01 +0100152 * @return output of salt command
153 */
Jiri Broulik16e9ce72017-05-17 13:28:31 +0200154def cmdRun(master, target, cmd, checkResponse = true, batch=null, output = true) {
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100155 def common = new com.mirantis.mk.Common()
Jakub Josef053df392017-05-03 15:51:05 +0200156 def originalCmd = cmd
Tomáš Kukráldfd4b492017-03-02 12:08:50 +0100157 common.infoMsg("Running command ${cmd} on ${target}")
Jakub Josef053df392017-05-03 15:51:05 +0200158 if (checkResponse) {
159 cmd = cmd + " && echo Salt command execution success"
160 }
Jiri Broulik16e9ce72017-05-17 13:28:31 +0200161 def out = runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'cmd.run', batch, [cmd])
Jakub Josef053df392017-05-03 15:51:05 +0200162 if (checkResponse) {
163 // iterate over all affected nodes and check success return code
Jiri Broulik16e9ce72017-05-17 13:28:31 +0200164 if (out["return"]){
165 for(int i=0;i<out["return"].size();i++){
166 def node = out["return"][i];
Jakub Josef053df392017-05-03 15:51:05 +0200167 for(int j=0;j<node.size();j++){
168 def nodeKey = node.keySet()[j]
169 if (!node[nodeKey].contains("Salt command execution success")) {
170 throw new Exception("Execution of cmd ${originalCmd} failed. Server returns: ${node[nodeKey]}")
171 }
172 }
173 }
174 }else{
175 throw new Exception("Salt Api response doesn't have return param!")
176 }
177 }
Jiri Broulik16e9ce72017-05-17 13:28:31 +0200178 if (output == true) {
179 printSaltCommandResult(out)
180 }
181 return out
Jakub Josef79ecec32017-02-17 14:36:28 +0100182}
183
Jakub Josef5ade54c2017-03-10 16:14:01 +0100184/**
185 * Perform complete salt sync between master and target
186 * @param master Salt connection object
187 * @param target Get pillar target
188 * @return output of salt command
189 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100190def syncAll(master, target) {
Filip Pytloun5a7f7fd2017-02-27 18:50:25 +0100191 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'saltutil.sync_all')
Jakub Josef79ecec32017-02-17 14:36:28 +0100192}
193
Jakub Josef5ade54c2017-03-10 16:14:01 +0100194/**
195 * Enforce highstate on given targets
196 * @param master Salt connection object
197 * @param target Highstate enforcing target
198 * @param output print output (optional, default true)
199 * @param failOnError throw exception on salt state result:false (optional, default true)
Jakub Josef2f25cf22017-03-28 13:34:57 +0200200 * @param batch salt batch parameter integer or string with percents (optional, default null - disable batch)
Jakub Josef5ade54c2017-03-10 16:14:01 +0100201 * @return output of salt command
202 */
Jakub Josef2f25cf22017-03-28 13:34:57 +0200203def enforceHighstate(master, target, output = false, failOnError = true, batch = null) {
204 def out = runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'state.highstate', batch)
Jakub Josef374beb72017-04-27 15:45:09 +0200205 checkResult(out, failOnError, output)
Jakub Josef79ecec32017-02-17 14:36:28 +0100206 return out
207}
208
Jakub Josef5ade54c2017-03-10 16:14:01 +0100209/**
Ales Komarek5276ebe2017-03-16 08:46:34 +0100210 * Get running minions IDs according to the target
211 * @param master Salt connection object
212 * @param target Get minions target
213 * @return list of active minions fitin
214 */
215def getMinions(master, target) {
216 def minionsRaw = runSaltCommand(master, 'local', target, 'test.ping')
217 return new ArrayList<String>(minionsRaw['return'][0].keySet())
218}
219
220
221/**
Jakub Josef5ade54c2017-03-10 16:14:01 +0100222 * Generates node key using key.gen_accept call
223 * @param master Salt connection object
224 * @param target Key generating target
225 * @param host Key generating host
226 * @param keysize generated key size (optional, default 4096)
227 * @return output of salt command
228 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100229def generateNodeKey(master, target, host, keysize = 4096) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100230 return runSaltCommand(master, 'wheel', target, 'key.gen_accept', [host], ['keysize': keysize])
Jakub Josef79ecec32017-02-17 14:36:28 +0100231}
232
Jakub Josef5ade54c2017-03-10 16:14:01 +0100233/**
Jakub Josef2f25cf22017-03-28 13:34:57 +0200234 * Generates node reclass metadata
Jakub Josef5ade54c2017-03-10 16:14:01 +0100235 * @param master Salt connection object
236 * @param target Metadata generating target
237 * @param host Metadata generating host
238 * @param classes Reclass classes
239 * @param parameters Reclass parameters
240 * @return output of salt command
241 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100242def generateNodeMetadata(master, target, host, classes, parameters) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100243 return runSaltCommand(master, 'local', target, 'reclass.node_create', [host, '_generated'], ['classes': classes, 'parameters': parameters])
Jakub Josef79ecec32017-02-17 14:36:28 +0100244}
245
Jakub Josef5ade54c2017-03-10 16:14:01 +0100246/**
247 * Run salt orchestrate on given targets
248 * @param master Salt connection object
249 * @param target Orchestration target
250 * @param orchestrate Salt orchestrate params
251 * @return output of salt command
252 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100253def orchestrateSystem(master, target, orchestrate) {
254 return runSaltCommand(master, 'runner', target, 'state.orchestrate', [orchestrate])
255}
256
Jakub Josef5ade54c2017-03-10 16:14:01 +0100257/**
258 * Run salt process step
259 * @param master Salt connection object
260 * @param tgt Salt process step target
261 * @param fun Salt process step function
262 * @param arg process step arguments (optional, default [])
Jakub Josef2f25cf22017-03-28 13:34:57 +0200263 * @param batch salt batch parameter integer or string with percents (optional, default null - disable batch)
Jakub Josef5ade54c2017-03-10 16:14:01 +0100264 * @param output print output (optional, default false)
265 * @return output of salt command
266 */
Tomáš Kukrálf5dda642017-03-02 14:22:59 +0100267def runSaltProcessStep(master, tgt, fun, arg = [], batch = null, output = false) {
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100268 def common = new com.mirantis.mk.Common()
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100269 def out
270
Tomas Kukrale90bb342017-03-02 21:30:35 +0000271 common.infoMsg("Running step ${fun} on ${tgt}")
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100272
Filip Pytlounf0435c02017-03-02 17:48:54 +0100273 if (batch == true) {
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100274 out = runSaltCommand(master, 'local_batch', ['expression': tgt, 'type': 'compound'], fun, String.valueOf(batch), arg)
275 } else {
276 out = runSaltCommand(master, 'local', ['expression': tgt, 'type': 'compound'], fun, batch, arg)
Jakub Josef79ecec32017-02-17 14:36:28 +0100277 }
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100278
Tomáš Kukrálf5dda642017-03-02 14:22:59 +0100279 if (output == true) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100280 printSaltCommandResult(out)
Jakub Josef79ecec32017-02-17 14:36:28 +0100281 }
Jiri Broulikae19c262017-05-16 19:06:52 +0200282 return out
Jakub Josef79ecec32017-02-17 14:36:28 +0100283}
284
285/**
286 * Check result for errors and throw exception if any found
287 *
288 * @param result Parsed response of Salt API
Jakub Josef8021c002017-03-27 15:41:28 +0200289 * @param failOnError Do you want to throw exception if salt-call fails (optional, default true)
Jakub Josefa87941c2017-04-20 17:14:58 +0200290 * @param printResults Do you want to print salt results (optional, default true)
Jakub Josefa87941c2017-04-20 17:14:58 +0200291 * @param printOnlyChanges If true (default), print only changed resources
Jakub Josef79ecec32017-02-17 14:36:28 +0100292 */
Jakub Josef374beb72017-04-27 15:45:09 +0200293def checkResult(result, failOnError = true, printResults = true, printOnlyChanges = true) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100294 def common = new com.mirantis.mk.Common()
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100295 if(result != null){
296 if(result['return']){
297 for (int i=0;i<result['return'].size();i++) {
298 def entry = result['return'][i]
299 if (!entry) {
300 if (failOnError) {
301 throw new Exception("Salt API returned empty response: ${result}")
302 } else {
303 common.errorMsg("Salt API returned empty response: ${result}")
Jakub Josefece32af2017-03-14 19:20:08 +0100304 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100305 }
306 for (int j=0;j<entry.size();j++) {
307 def nodeKey = entry.keySet()[j]
308 def node=entry[nodeKey]
Jakub Josefa87941c2017-04-20 17:14:58 +0200309 def outputResources = []
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100310 common.infoMsg("Node ${nodeKey} changes:")
311 if(node instanceof Map || node instanceof List){
312 for (int k=0;k<node.size();k++) {
313 def resource;
314 def resKey;
315 if(node instanceof Map){
316 resKey = node.keySet()[k]
317 }else if(node instanceof List){
318 resKey = k
319 }
320 resource = node[resKey]
Jakub Josefc4c40202017-04-28 12:04:24 +0200321 // print
Jakub Josefa87941c2017-04-20 17:14:58 +0200322 if(printResults){
323 if(resource instanceof Map && resource.keySet().contains("result")){
324 //clean unnesaccary fields
325 if(resource.keySet().contains("__run_num__")){
326 resource.remove("__run_num__")
327 }
328 if(resource.keySet().contains("__id__")){
329 resource.remove("__id__")
330 }
331 if(resource.keySet().contains("pchanges")){
332 resource.remove("pchanges")
333 }
334 if(!resource["result"] || (resource["result"] instanceof String && resource["result"] != "true")){
335 if(resource["result"] != null){
336 outputResources.add(String.format("Resource: %s\n\u001B[31m%s\u001B[0m", resKey, common.prettyPrint(resource)))
337 }else{
338 outputResources.add(String.format("Resource: %s\n\u001B[33m%s\u001B[0m", resKey, common.prettyPrint(resource)))
339 }
340 }else{
341 if(!printOnlyChanges || resource.changes.size() > 0){
342 outputResources.add(String.format("Resource: %s\n\u001B[32m%s\u001B[0m", resKey, common.prettyPrint(resource)))
343 }
344 }
345 }else{
346 outputResources.add(String.format("Resource: %s\n\u001B[36m%s\u001B[0m", resKey, common.prettyPrint(resource)))
347 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100348 }
Jakub Josefc4c40202017-04-28 12:04:24 +0200349 common.debugMsg("checkResult: checking resource: ${resource}")
350 if(resource instanceof String || (resource["result"] != null && !resource["result"]) || (resource["result"] instanceof String && resource["result"] == "false")){
351 def prettyResource = common.prettyPrint(resource)
352 if(env["ASK_ON_ERROR"] && env["ASK_ON_ERROR"] == "true"){
353 timeout(time:1, unit:'HOURS') {
354 input message: "False result on ${nodeKey} found, resource ${prettyResource}. \nDo you want to continue?"
355 }
356 }else{
Jakub Josefd97d7db2017-05-11 19:11:53 +0200357 common.errorMsg(String.format("Resource: %s\n%s", resKey, prettyResource))
Jakub Josefd9001df2017-05-11 16:45:28 +0200358 def errorMsg = "Salt state on node ${nodeKey} failed: ${prettyResource}."
Jakub Josefc4c40202017-04-28 12:04:24 +0200359 if (failOnError) {
360 throw new Exception(errorMsg)
361 } else {
362 common.errorMsg(errorMsg)
363 }
364 }
365 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100366 }
Jakub Josefa87941c2017-04-20 17:14:58 +0200367 }else if(node!=null && node!=""){
368 outputResources.add(String.format("Resource: %s\n\u001B[36m%s\u001B[0m", resKey, common.prettyPrint(node)))
369 }
370 if(printResults && !outputResources.isEmpty()){
Jakub Josefb77c0812017-03-27 14:11:01 +0200371 wrap([$class: 'AnsiColorBuildWrapper']) {
372 print outputResources.stream().collect(Collectors.joining("\n"))
373 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100374 }
375 }
Jakub Josef52f69f72017-03-14 15:18:08 +0100376 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100377 }else{
378 common.errorMsg("Salt result hasn't return attribute! Result: ${result}")
Jakub Josef79ecec32017-02-17 14:36:28 +0100379 }
Jakub Josef52f69f72017-03-14 15:18:08 +0100380 }else{
Jakub Josefa87941c2017-04-20 17:14:58 +0200381 common.errorMsg("Cannot check salt result, given result is null")
Jakub Josef79ecec32017-02-17 14:36:28 +0100382 }
383}
384
385/**
Jakub Josef7852fe12017-03-15 16:02:41 +0100386 * Print salt command run results in human-friendly form
Jakub Josef79ecec32017-02-17 14:36:28 +0100387 *
388 * @param result Parsed response of Salt API
Jakub Josef79ecec32017-02-17 14:36:28 +0100389 */
Filip Pytlound2f1bbe2017-02-27 19:03:51 +0100390def printSaltCommandResult(result) {
Jakub Josef871bf152017-03-14 20:13:41 +0100391 def common = new com.mirantis.mk.Common()
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100392 if(result != null){
393 if(result['return']){
394 for (int i=0; i<result['return'].size(); i++) {
395 def entry = result['return'][i]
396 for (int j=0; j<entry.size(); j++) {
397 common.debugMsg("printSaltCommandResult: printing salt command entry: ${entry}")
398 def nodeKey = entry.keySet()[j]
399 def node=entry[nodeKey]
Jakub Josefb41c8d52017-03-24 13:52:24 +0100400 common.infoMsg(String.format("Node %s changes:\n%s",nodeKey, common.prettyPrint(node)))
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100401 }
Jakub Josef8a715bf2017-03-14 21:39:01 +0100402 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100403 }else{
404 common.errorMsg("Salt result hasn't return attribute! Result: ${result}")
Jakub Josef79ecec32017-02-17 14:36:28 +0100405 }
Jakub Josef8a715bf2017-03-14 21:39:01 +0100406 }else{
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100407 common.errorMsg("Cannot print salt command result, given result is null")
Jakub Josef52f69f72017-03-14 15:18:08 +0100408 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100409}
Tomáš Kukrálb12eedd2017-04-21 10:45:13 +0200410
411
412/**
413 * Return content of file target
414 *
415 * @param master Salt master object
416 * @param target Compound target (should target only one host)
417 * @param file File path to read (/etc/hosts for example)
418 */
419
420def getFileContent(master, target, file) {
421 result = cmdRun(master, target, "cat ${file}")
422 return result['return'][0].values()[0]
423}