blob: 2d97913c4ce221c059c7b545d1d4a2913d6994f9 [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")
Filip Pytlounf0435c02017-03-02 17:48:54 +010052 * @param batch
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
Filip Pytlounf0435c02017-03-02 17:48:54 +010067 if (batch == true) {
68 data['batch'] = "local_batch"
Jakub Josef79ecec32017-02-17 14:36:28 +010069 }
70
71 if (args) {
72 data['arg'] = args
73 }
74
75 if (kwargs) {
76 data['kwarg'] = kwargs
77 }
78
79 headers = [
80 'X-Auth-Token': "${master.authToken}"
81 ]
82
83 return http.sendHttpPostRequest("${master.url}/", data, headers)
84}
85
Jakub Josef5ade54c2017-03-10 16:14:01 +010086/**
87 * Return pillar for given master and target
88 * @param master Salt connection object
89 * @param target Get pillar target
90 * @param pillar pillar name (optional)
91 * @return output of salt command
92 */
Ales Komarekcec24d42017-03-08 10:25:45 +010093def getPillar(master, target, pillar = null) {
Tomáš Kukráld2589702017-03-10 16:30:46 +010094 if (pillar != null) {
Jakub Josef5ade54c2017-03-10 16:14:01 +010095 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'pillar.get', null, [pillar.replace('.', ':')])
Tomáš Kukráld2589702017-03-10 16:30:46 +010096 } else {
Jakub Josef5ade54c2017-03-10 16:14:01 +010097 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'pillar.data')
Ales Komareka3c7e502017-03-13 11:20:44 +010098 }
Jakub Josef79ecec32017-02-17 14:36:28 +010099}
100
Jakub Josef5ade54c2017-03-10 16:14:01 +0100101/**
102 * Return grain for given master and target
103 * @param master Salt connection object
104 * @param target Get grain target
105 * @param grain grain name (optional)
106 * @return output of salt command
107 */
Ales Komarekcec24d42017-03-08 10:25:45 +0100108def getGrain(master, target, grain = null) {
109 if(grain != null) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100110 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'grain.item', null, [grain])
111 } else {
112 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'grain.items')
Ales Komarekcec24d42017-03-08 10:25:45 +0100113 }
Ales Komarekcec24d42017-03-08 10:25:45 +0100114}
115
Jakub Josef5ade54c2017-03-10 16:14:01 +0100116/**
117 * Enforces state on given master and target
118 * @param master Salt connection object
119 * @param target State enforcing target
120 * @param state Salt state
121 * @param output print output (optional, default true)
122 * @param failOnError throw exception on salt state result:false (optional, default true)
123 * @return output of salt command
124 */
125def enforceState(master, target, state, output = true, failOnError = true) {
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100126 def common = new com.mirantis.mk.Common()
Jakub Josef79ecec32017-02-17 14:36:28 +0100127 def run_states
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100128
Jakub Josef79ecec32017-02-17 14:36:28 +0100129 if (state instanceof String) {
130 run_states = state
131 } else {
132 run_states = state.join(',')
133 }
134
Tomáš Kukráldfd4b492017-03-02 12:08:50 +0100135 common.infoMsg("Enforcing state ${run_states} on ${target}")
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100136
Filip Pytloun5a7f7fd2017-02-27 18:50:25 +0100137 def out = runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'state.sls', null, [run_states])
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100138
Jakub Josefc952e5a2017-03-23 15:02:12 +0100139 if (output == true) {
140 printSaltStateResult(out)
Jakub Josef79ecec32017-02-17 14:36:28 +0100141 }
Jakub Josef8021c002017-03-27 15:41:28 +0200142 checkResult(out, failOnError, !output)
Jakub Josef79ecec32017-02-17 14:36:28 +0100143 return out
144}
145
Jakub Josef5ade54c2017-03-10 16:14:01 +0100146/**
147 * Run command on salt minion (salt cmd.run wrapper)
148 * @param master Salt connection object
149 * @param target Get pillar target
150 * @param cmd command
151 * @return output of salt command
152 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100153def cmdRun(master, target, cmd) {
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100154 def common = new com.mirantis.mk.Common()
155
Tomáš Kukráldfd4b492017-03-02 12:08:50 +0100156 common.infoMsg("Running command ${cmd} on ${target}")
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100157
Jakub Josef5ade54c2017-03-10 16:14:01 +0100158 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'cmd.run', null, [cmd])
Jakub Josef79ecec32017-02-17 14:36:28 +0100159}
160
Jakub Josef5ade54c2017-03-10 16:14:01 +0100161/**
162 * Perform complete salt sync between master and target
163 * @param master Salt connection object
164 * @param target Get pillar target
165 * @return output of salt command
166 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100167def syncAll(master, target) {
Filip Pytloun5a7f7fd2017-02-27 18:50:25 +0100168 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'saltutil.sync_all')
Jakub Josef79ecec32017-02-17 14:36:28 +0100169}
170
Jakub Josef5ade54c2017-03-10 16:14:01 +0100171/**
172 * Enforce highstate on given targets
173 * @param master Salt connection object
174 * @param target Highstate enforcing target
175 * @param output print output (optional, default true)
176 * @param failOnError throw exception on salt state result:false (optional, default true)
177 * @return output of salt command
178 */
179def enforceHighstate(master, target, output = false, failOnError = true) {
Filip Pytloun5a7f7fd2017-02-27 18:50:25 +0100180 def out = runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'state.highstate')
Jakub Josefc952e5a2017-03-23 15:02:12 +0100181 if (output == true) {
182 printSaltStateResult(out)
Jakub Josef79ecec32017-02-17 14:36:28 +0100183 }
Jakub Josef8021c002017-03-27 15:41:28 +0200184 checkResult(out, failOnError, !output)
Jakub Josef79ecec32017-02-17 14:36:28 +0100185 return out
186}
187
Jakub Josef5ade54c2017-03-10 16:14:01 +0100188/**
Ales Komarek5276ebe2017-03-16 08:46:34 +0100189 * Get running minions IDs according to the target
190 * @param master Salt connection object
191 * @param target Get minions target
192 * @return list of active minions fitin
193 */
194def getMinions(master, target) {
195 def minionsRaw = runSaltCommand(master, 'local', target, 'test.ping')
196 return new ArrayList<String>(minionsRaw['return'][0].keySet())
197}
198
199
200/**
Jakub Josef5ade54c2017-03-10 16:14:01 +0100201 * Generates node key using key.gen_accept call
202 * @param master Salt connection object
203 * @param target Key generating target
204 * @param host Key generating host
205 * @param keysize generated key size (optional, default 4096)
206 * @return output of salt command
207 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100208def generateNodeKey(master, target, host, keysize = 4096) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100209 return runSaltCommand(master, 'wheel', target, 'key.gen_accept', [host], ['keysize': keysize])
Jakub Josef79ecec32017-02-17 14:36:28 +0100210}
211
Jakub Josef5ade54c2017-03-10 16:14:01 +0100212/**
213 * Generates node reclass metadata
214 * @param master Salt connection object
215 * @param target Metadata generating target
216 * @param host Metadata generating host
217 * @param classes Reclass classes
218 * @param parameters Reclass parameters
219 * @return output of salt command
220 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100221def generateNodeMetadata(master, target, host, classes, parameters) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100222 return runSaltCommand(master, 'local', target, 'reclass.node_create', [host, '_generated'], ['classes': classes, 'parameters': parameters])
Jakub Josef79ecec32017-02-17 14:36:28 +0100223}
224
Jakub Josef5ade54c2017-03-10 16:14:01 +0100225/**
226 * Run salt orchestrate on given targets
227 * @param master Salt connection object
228 * @param target Orchestration target
229 * @param orchestrate Salt orchestrate params
230 * @return output of salt command
231 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100232def orchestrateSystem(master, target, orchestrate) {
233 return runSaltCommand(master, 'runner', target, 'state.orchestrate', [orchestrate])
234}
235
Jakub Josef5ade54c2017-03-10 16:14:01 +0100236/**
237 * Run salt process step
238 * @param master Salt connection object
239 * @param tgt Salt process step target
240 * @param fun Salt process step function
241 * @param arg process step arguments (optional, default [])
242 * @param batch using batch (optional, default false)
243 * @param output print output (optional, default false)
244 * @return output of salt command
245 */
Tomáš Kukrálf5dda642017-03-02 14:22:59 +0100246def runSaltProcessStep(master, tgt, fun, arg = [], batch = null, output = false) {
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100247 def common = new com.mirantis.mk.Common()
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100248 def out
249
Tomas Kukrale90bb342017-03-02 21:30:35 +0000250 common.infoMsg("Running step ${fun} on ${tgt}")
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100251
Filip Pytlounf0435c02017-03-02 17:48:54 +0100252 if (batch == true) {
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100253 out = runSaltCommand(master, 'local_batch', ['expression': tgt, 'type': 'compound'], fun, String.valueOf(batch), arg)
254 } else {
255 out = runSaltCommand(master, 'local', ['expression': tgt, 'type': 'compound'], fun, batch, arg)
Jakub Josef79ecec32017-02-17 14:36:28 +0100256 }
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100257
Tomáš Kukrálf5dda642017-03-02 14:22:59 +0100258 if (output == true) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100259 printSaltCommandResult(out)
Jakub Josef79ecec32017-02-17 14:36:28 +0100260 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100261}
262
263/**
264 * Check result for errors and throw exception if any found
265 *
266 * @param result Parsed response of Salt API
Jakub Josef8021c002017-03-27 15:41:28 +0200267 * @param failOnError Do you want to throw exception if salt-call fails (optional, default true)
268 * @param printStateOnError Do you want to print failed state on error (optional, default true)
Jakub Josef79ecec32017-02-17 14:36:28 +0100269 */
Jakub Josef8021c002017-03-27 15:41:28 +0200270def checkResult(result, failOnError = true, printStateOnError = true) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100271 def common = new com.mirantis.mk.Common()
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100272 if(result != null){
273 if(result['return']){
274 for (int i=0;i<result['return'].size();i++) {
275 def entry = result['return'][i]
276 if (!entry) {
277 if (failOnError) {
278 throw new Exception("Salt API returned empty response: ${result}")
279 } else {
280 common.errorMsg("Salt API returned empty response: ${result}")
Jakub Josefece32af2017-03-14 19:20:08 +0100281 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100282 }
283 for (int j=0;j<entry.size();j++) {
284 def nodeKey = entry.keySet()[j]
285 def node=entry[nodeKey]
286 for (int k=0;k<node.size();k++) {
287 def resource;
288 def resKey;
289 if(node instanceof Map){
290 resKey = node.keySet()[k]
291 }else if(node instanceof List){
292 resKey = k
293 }
294 resource = node[resKey]
295 common.debugMsg("checkResult: checking resource: ${resource}")
296 if(resource instanceof String || !resource["result"] || (resource["result"] instanceof String && resource["result"] != "true")){
Jakub Josef43f62bc2017-03-24 14:13:09 +0100297 if(env["ASK_ON_ERROR"] && env["ASK_ON_ERROR"] == "true"){
Jakub Josefb41c8d52017-03-24 13:52:24 +0100298 def prettyResource = common.prettyPrint(resource)
Jakub Josefc952e5a2017-03-23 15:02:12 +0100299 timeout(time:1, unit:'HOURS') {
Jakub Josef77ba3b22017-03-23 15:55:14 +0100300 input message: "False result on ${nodeKey} found, resource ${prettyResource}. \nDo you want to continue?"
Jakub Josefc952e5a2017-03-23 15:02:12 +0100301 }
302 }else{
Jakub Josef8021c002017-03-27 15:41:28 +0200303 def errorMsg
304 if(printStateOnError){
305 errorMsg = "Salt state on node ${nodeKey} failed: ${resource}. State output: ${node}"
306 }else{
307 errorMsg = "Salt state on node ${nodeKey} failed. Look on error upper."
308 }
Jakub Josefc952e5a2017-03-23 15:02:12 +0100309 if (failOnError) {
Jakub Josef8021c002017-03-27 15:41:28 +0200310 throw new Exception(errorMsg)
Jakub Josefc952e5a2017-03-23 15:02:12 +0100311 } else {
Jakub Josef8021c002017-03-27 15:41:28 +0200312 common.errorMsg(errorMsg)
Jakub Josefc952e5a2017-03-23 15:02:12 +0100313 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100314 }
Jakub Josef52f69f72017-03-14 15:18:08 +0100315 }
Jakub Josef5ade54c2017-03-10 16:14:01 +0100316 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100317 }
318 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100319 }else{
320 common.errorMsg("Salt result hasn't return attribute! Result: ${result}")
Jakub Josef79ecec32017-02-17 14:36:28 +0100321 }
Jakub Josef52f69f72017-03-14 15:18:08 +0100322 }else{
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100323 common.errorMsg("Cannot check salt result, given result is null")
Jakub Josef79ecec32017-02-17 14:36:28 +0100324 }
325}
326
327/**
328 * Print Salt state run results in human-friendly form
329 *
330 * @param result Parsed response of Salt API
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100331 * @param onlyChanges If true (default), print only changed resources
Jakub Josef79ecec32017-02-17 14:36:28 +0100332 */
333def printSaltStateResult(result, onlyChanges = true) {
Tomáš Kukrál6eb45f82017-03-08 18:26:16 +0100334 def common = new com.mirantis.mk.Common()
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100335 if(result != null){
336 if(result['return']){
337 for (int i=0; i<result['return'].size(); i++) {
338 def entry = result['return'][i]
339 for (int j=0; j<entry.size(); j++) {
340 common.debugMsg("printSaltStateResult: printing salt command entry: ${entry}")
341 def nodeKey = entry.keySet()[j]
342 def node=entry[nodeKey]
343 common.infoMsg("Node ${nodeKey} changes:")
344 if(node instanceof Map || node instanceof List){
Jakub Josefb77c0812017-03-27 14:11:01 +0200345 def outputResources = []
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100346 for (int k=0;k<node.size();k++) {
347 def resource;
348 def resKey;
349 if(node instanceof Map){
350 resKey = node.keySet()[k]
351 }else if(node instanceof List){
352 resKey = k
353 }
354 resource = node[resKey]
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100355 if(resource instanceof Map && resource.keySet().contains("result")){
Jakub Joseff1fb4472017-03-16 14:11:54 +0100356 //clean unnesaccary fields
357 if(resource.keySet().contains("__run_num__")){
358 resource.remove("__run_num__")
359 }
360 if(resource.keySet().contains("__id__")){
361 resource.remove("__id__")
362 }
363 if(resource.keySet().contains("pchanges")){
364 resource.remove("pchanges")
365 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100366 if(!resource["result"] || (resource["result"] instanceof String && resource["result"] != "true")){
Jakub Josef0e7bd632017-03-16 16:25:05 +0100367 if(resource["result"] != null){
Jakub Josef8021c002017-03-27 15:41:28 +0200368 outputResources.add(String.format("Resource: %s\n\u001B[31m%s\u001B[0m", resKey, common.prettyPrint(resource)))
Jakub Josef0e7bd632017-03-16 16:25:05 +0100369 }else{
Jakub Josef8021c002017-03-27 15:41:28 +0200370 outputResources.add(String.format("Resource: %s\n\u001B[33m%s\u001B[0m", resKey, common.prettyPrint(resource)))
Jakub Josef0e7bd632017-03-16 16:25:05 +0100371 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100372 }else{
373 if(!onlyChanges || resource.changes.size() > 0){
Jakub Josef8021c002017-03-27 15:41:28 +0200374 outputResources.add(String.format("Resource: %s\n\u001B[32m%s\u001B[0m", resKey, common.prettyPrint(resource)))
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100375 }
376 }
377 }else{
Jakub Josef8021c002017-03-27 15:41:28 +0200378 outputResources.add(String.format("Resource: %s\n\u001B[36m%s\u001B[0m", resKey, common.prettyPrint(resource)))
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100379 }
380 }
Jakub Josefb77c0812017-03-27 14:11:01 +0200381 wrap([$class: 'AnsiColorBuildWrapper']) {
382 print outputResources.stream().collect(Collectors.joining("\n"))
383 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100384 }else{
Jakub Josefb41c8d52017-03-24 13:52:24 +0100385 common.infoMsg(common.prettyPrint(node))
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100386 }
387 }
Jakub Josef52f69f72017-03-14 15:18:08 +0100388 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100389 }else{
390 common.errorMsg("Salt result hasn't return attribute! Result: ${result}")
Jakub Josef79ecec32017-02-17 14:36:28 +0100391 }
Jakub Josef52f69f72017-03-14 15:18:08 +0100392 }else{
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100393 common.errorMsg("Cannot print salt state result, given result is null")
Jakub Josef79ecec32017-02-17 14:36:28 +0100394 }
395}
396
397/**
Jakub Josef7852fe12017-03-15 16:02:41 +0100398 * Print salt command run results in human-friendly form
Jakub Josef79ecec32017-02-17 14:36:28 +0100399 *
400 * @param result Parsed response of Salt API
Jakub Josef79ecec32017-02-17 14:36:28 +0100401 */
Filip Pytlound2f1bbe2017-02-27 19:03:51 +0100402def printSaltCommandResult(result) {
Jakub Josef871bf152017-03-14 20:13:41 +0100403 def common = new com.mirantis.mk.Common()
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100404 if(result != null){
405 if(result['return']){
406 for (int i=0; i<result['return'].size(); i++) {
407 def entry = result['return'][i]
408 for (int j=0; j<entry.size(); j++) {
409 common.debugMsg("printSaltCommandResult: printing salt command entry: ${entry}")
410 def nodeKey = entry.keySet()[j]
411 def node=entry[nodeKey]
Jakub Josefb41c8d52017-03-24 13:52:24 +0100412 common.infoMsg(String.format("Node %s changes:\n%s",nodeKey, common.prettyPrint(node)))
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100413 }
Jakub Josef8a715bf2017-03-14 21:39:01 +0100414 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100415 }else{
416 common.errorMsg("Salt result hasn't return attribute! Result: ${result}")
Jakub Josef79ecec32017-02-17 14:36:28 +0100417 }
Jakub Josef8a715bf2017-03-14 21:39:01 +0100418 }else{
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100419 common.errorMsg("Cannot print salt command result, given result is null")
Jakub Josef52f69f72017-03-14 15:18:08 +0100420 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100421}