blob: b880b0b1d55de3a5ff37e6406519a8821e99592b [file] [log] [blame]
Jakub Josef79ecec32017-02-17 14:36:28 +01001package com.mirantis.mk
2
Jakub Josefd9afd0e2017-03-15 19:19:23 +01003import static groovy.json.JsonOutput.prettyPrint
4import static groovy.json.JsonOutput.toJson
Jakub Josef935d2ef2017-03-15 17:35:51 +01005
Jakub Josef79ecec32017-02-17 14:36:28 +01006/**
7 * Salt functions
8 *
9*/
10
11/**
12 * Salt connection and context parameters
13 *
14 * @param url Salt API server URL
15 * @param credentialsID ID of credentials store entry
16 */
17def connection(url, credentialsId = "salt") {
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +010018 def common = new com.mirantis.mk.Common()
Jakub Josef79ecec32017-02-17 14:36:28 +010019 params = [
20 "url": url,
21 "credentialsId": credentialsId,
22 "authToken": null,
23 "creds": common.getCredentials(credentialsId)
24 ]
25 params["authToken"] = saltLogin(params)
26
27 return params
28}
29
30/**
31 * Login to Salt API, return auth token
32 *
33 * @param master Salt connection object
34 */
35def saltLogin(master) {
Tomáš Kukrál7bec0532017-02-20 15:39:31 +010036 def http = new com.mirantis.mk.Http()
Jakub Josef79ecec32017-02-17 14:36:28 +010037 data = [
38 'username': master.creds.username,
39 'password': master.creds.password.toString(),
40 'eauth': 'pam'
41 ]
Tomáš Kukrál7bec0532017-02-20 15:39:31 +010042 authToken = http.restGet(master, '/login', data)['return'][0]['token']
Jakub Josef79ecec32017-02-17 14:36:28 +010043 return authToken
44}
45
46/**
47 * Run action using Salt API
48 *
49 * @param master Salt connection object
50 * @param client Client type
51 * @param target Target specification, eg. for compound matches by Pillar
52 * data: ['expression': 'I@openssh:server', 'type': 'compound'])
53 * @param function Function to execute (eg. "state.sls")
Filip Pytlounf0435c02017-03-02 17:48:54 +010054 * @param batch
Jakub Josef79ecec32017-02-17 14:36:28 +010055 * @param args Additional arguments to function
56 * @param kwargs Additional key-value arguments to function
57 */
58@NonCPS
59def runSaltCommand(master, client, target, function, batch = null, args = null, kwargs = null) {
iberezovskiyd4240b52017-02-20 17:18:28 +040060 def http = new com.mirantis.mk.Http()
Jakub Josef79ecec32017-02-17 14:36:28 +010061
62 data = [
63 'tgt': target.expression,
64 'fun': function,
65 'client': client,
66 'expr_form': target.type,
67 ]
68
Filip Pytlounf0435c02017-03-02 17:48:54 +010069 if (batch == true) {
70 data['batch'] = "local_batch"
Jakub Josef79ecec32017-02-17 14:36:28 +010071 }
72
73 if (args) {
74 data['arg'] = args
75 }
76
77 if (kwargs) {
78 data['kwarg'] = kwargs
79 }
80
81 headers = [
82 'X-Auth-Token': "${master.authToken}"
83 ]
84
85 return http.sendHttpPostRequest("${master.url}/", data, headers)
86}
87
Jakub Josef5ade54c2017-03-10 16:14:01 +010088/**
89 * Return pillar for given master and target
90 * @param master Salt connection object
91 * @param target Get pillar target
92 * @param pillar pillar name (optional)
93 * @return output of salt command
94 */
Ales Komarekcec24d42017-03-08 10:25:45 +010095def getPillar(master, target, pillar = null) {
Tomáš Kukráld2589702017-03-10 16:30:46 +010096 if (pillar != null) {
Jakub Josef5ade54c2017-03-10 16:14:01 +010097 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'pillar.get', null, [pillar.replace('.', ':')])
Tomáš Kukráld2589702017-03-10 16:30:46 +010098 } else {
Jakub Josef5ade54c2017-03-10 16:14:01 +010099 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'pillar.data')
Ales Komareka3c7e502017-03-13 11:20:44 +0100100 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100101}
102
Jakub Josef5ade54c2017-03-10 16:14:01 +0100103/**
104 * Return grain for given master and target
105 * @param master Salt connection object
106 * @param target Get grain target
107 * @param grain grain name (optional)
108 * @return output of salt command
109 */
Ales Komarekcec24d42017-03-08 10:25:45 +0100110def getGrain(master, target, grain = null) {
111 if(grain != null) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100112 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'grain.item', null, [grain])
113 } else {
114 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'grain.items')
Ales Komarekcec24d42017-03-08 10:25:45 +0100115 }
Ales Komarekcec24d42017-03-08 10:25:45 +0100116}
117
Jakub Josef5ade54c2017-03-10 16:14:01 +0100118/**
119 * Enforces state on given master and target
120 * @param master Salt connection object
121 * @param target State enforcing target
122 * @param state Salt state
123 * @param output print output (optional, default true)
124 * @param failOnError throw exception on salt state result:false (optional, default true)
125 * @return output of salt command
126 */
127def enforceState(master, target, state, output = true, failOnError = true) {
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
Filip Pytloun5a7f7fd2017-02-27 18:50:25 +0100139 def out = runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'state.sls', null, [run_states])
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100140
Jakub Josefc952e5a2017-03-23 15:02:12 +0100141 if (output == true) {
142 printSaltStateResult(out)
Jakub Josef79ecec32017-02-17 14:36:28 +0100143 }
Jakub Josefc952e5a2017-03-23 15:02:12 +0100144 checkResult(out, failOnError)
Jakub Josef79ecec32017-02-17 14:36:28 +0100145 return out
146}
147
Jakub Josef5ade54c2017-03-10 16:14:01 +0100148/**
149 * Run command on salt minion (salt cmd.run wrapper)
150 * @param master Salt connection object
151 * @param target Get pillar target
152 * @param cmd command
153 * @return output of salt command
154 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100155def cmdRun(master, target, cmd) {
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100156 def common = new com.mirantis.mk.Common()
157
Tomáš Kukráldfd4b492017-03-02 12:08:50 +0100158 common.infoMsg("Running command ${cmd} on ${target}")
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100159
Jakub Josef5ade54c2017-03-10 16:14:01 +0100160 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'cmd.run', null, [cmd])
Jakub Josef79ecec32017-02-17 14:36:28 +0100161}
162
Jakub Josef5ade54c2017-03-10 16:14:01 +0100163/**
164 * Perform complete salt sync between master and target
165 * @param master Salt connection object
166 * @param target Get pillar target
167 * @return output of salt command
168 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100169def syncAll(master, target) {
Filip Pytloun5a7f7fd2017-02-27 18:50:25 +0100170 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'saltutil.sync_all')
Jakub Josef79ecec32017-02-17 14:36:28 +0100171}
172
Jakub Josef5ade54c2017-03-10 16:14:01 +0100173/**
174 * Enforce highstate on given targets
175 * @param master Salt connection object
176 * @param target Highstate enforcing target
177 * @param output print output (optional, default true)
178 * @param failOnError throw exception on salt state result:false (optional, default true)
179 * @return output of salt command
180 */
181def enforceHighstate(master, target, output = false, failOnError = true) {
Filip Pytloun5a7f7fd2017-02-27 18:50:25 +0100182 def out = runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'state.highstate')
Jakub Josefc952e5a2017-03-23 15:02:12 +0100183 if (output == true) {
184 printSaltStateResult(out)
Jakub Josef79ecec32017-02-17 14:36:28 +0100185 }
Jakub Josefc952e5a2017-03-23 15:02:12 +0100186 checkResult(out, failOnError)
Jakub Josef79ecec32017-02-17 14:36:28 +0100187 return out
188}
189
Jakub Josef5ade54c2017-03-10 16:14:01 +0100190/**
Ales Komarek5276ebe2017-03-16 08:46:34 +0100191 * Get running minions IDs according to the target
192 * @param master Salt connection object
193 * @param target Get minions target
194 * @return list of active minions fitin
195 */
196def getMinions(master, target) {
197 def minionsRaw = runSaltCommand(master, 'local', target, 'test.ping')
198 return new ArrayList<String>(minionsRaw['return'][0].keySet())
199}
200
201
202/**
Jakub Josef5ade54c2017-03-10 16:14:01 +0100203 * Generates node key using key.gen_accept call
204 * @param master Salt connection object
205 * @param target Key generating target
206 * @param host Key generating host
207 * @param keysize generated key size (optional, default 4096)
208 * @return output of salt command
209 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100210def generateNodeKey(master, target, host, keysize = 4096) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100211 return runSaltCommand(master, 'wheel', target, 'key.gen_accept', [host], ['keysize': keysize])
Jakub Josef79ecec32017-02-17 14:36:28 +0100212}
213
Jakub Josef5ade54c2017-03-10 16:14:01 +0100214/**
215 * Generates node reclass metadata
216 * @param master Salt connection object
217 * @param target Metadata generating target
218 * @param host Metadata generating host
219 * @param classes Reclass classes
220 * @param parameters Reclass parameters
221 * @return output of salt command
222 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100223def generateNodeMetadata(master, target, host, classes, parameters) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100224 return runSaltCommand(master, 'local', target, 'reclass.node_create', [host, '_generated'], ['classes': classes, 'parameters': parameters])
Jakub Josef79ecec32017-02-17 14:36:28 +0100225}
226
Jakub Josef5ade54c2017-03-10 16:14:01 +0100227/**
228 * Run salt orchestrate on given targets
229 * @param master Salt connection object
230 * @param target Orchestration target
231 * @param orchestrate Salt orchestrate params
232 * @return output of salt command
233 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100234def orchestrateSystem(master, target, orchestrate) {
235 return runSaltCommand(master, 'runner', target, 'state.orchestrate', [orchestrate])
236}
237
Jakub Josef5ade54c2017-03-10 16:14:01 +0100238/**
239 * Run salt process step
240 * @param master Salt connection object
241 * @param tgt Salt process step target
242 * @param fun Salt process step function
243 * @param arg process step arguments (optional, default [])
244 * @param batch using batch (optional, default false)
245 * @param output print output (optional, default false)
246 * @return output of salt command
247 */
Tomáš Kukrálf5dda642017-03-02 14:22:59 +0100248def runSaltProcessStep(master, tgt, fun, arg = [], batch = null, output = false) {
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100249 def common = new com.mirantis.mk.Common()
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100250 def out
251
Tomas Kukrale90bb342017-03-02 21:30:35 +0000252 common.infoMsg("Running step ${fun} on ${tgt}")
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100253
Filip Pytlounf0435c02017-03-02 17:48:54 +0100254 if (batch == true) {
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100255 out = runSaltCommand(master, 'local_batch', ['expression': tgt, 'type': 'compound'], fun, String.valueOf(batch), arg)
256 } else {
257 out = runSaltCommand(master, 'local', ['expression': tgt, 'type': 'compound'], fun, batch, arg)
Jakub Josef79ecec32017-02-17 14:36:28 +0100258 }
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100259
Tomáš Kukrálf5dda642017-03-02 14:22:59 +0100260 if (output == true) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100261 printSaltCommandResult(out)
Jakub Josef79ecec32017-02-17 14:36:28 +0100262 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100263}
264
265/**
266 * Check result for errors and throw exception if any found
267 *
268 * @param result Parsed response of Salt API
Jakub Josef5ade54c2017-03-10 16:14:01 +0100269 * @param failOnError Do you want to throw exception if salt-call fails
Jakub Josef79ecec32017-02-17 14:36:28 +0100270 */
Jakub Josef5ade54c2017-03-10 16:14:01 +0100271def checkResult(result, failOnError = true) {
272 def common = new com.mirantis.mk.Common()
Jakub Josefc952e5a2017-03-23 15:02:12 +0100273 def askOnError = false
274 try {
275 askOnError = env.ASK_ON_ERROR
276 } catch (MissingPropertyException e) {
277 askOnError = false
278 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100279 if(result != null){
280 if(result['return']){
281 for (int i=0;i<result['return'].size();i++) {
282 def entry = result['return'][i]
283 if (!entry) {
284 if (failOnError) {
285 throw new Exception("Salt API returned empty response: ${result}")
286 } else {
287 common.errorMsg("Salt API returned empty response: ${result}")
Jakub Josefece32af2017-03-14 19:20:08 +0100288 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100289 }
290 for (int j=0;j<entry.size();j++) {
291 def nodeKey = entry.keySet()[j]
292 def node=entry[nodeKey]
293 for (int k=0;k<node.size();k++) {
294 def resource;
295 def resKey;
296 if(node instanceof Map){
297 resKey = node.keySet()[k]
298 }else if(node instanceof List){
299 resKey = k
300 }
301 resource = node[resKey]
302 common.debugMsg("checkResult: checking resource: ${resource}")
303 if(resource instanceof String || !resource["result"] || (resource["result"] instanceof String && resource["result"] != "true")){
Jakub Josefc952e5a2017-03-23 15:02:12 +0100304 if(askOnError){
305 timeout(time:1, unit:'HOURS') {
Jakub Josef760e84b2017-03-23 15:46:23 +0100306 input message: "False result on ${node} found, resource ${resource}. \nDo you want to continue?"
Jakub Josefc952e5a2017-03-23 15:02:12 +0100307 }
308 }else{
309 if (failOnError) {
310 throw new Exception("Salt state on node ${nodeKey} failed: ${resource}. State output: ${node}")
311 } else {
312 common.errorMsg("Salt state on node ${nodeKey} failed: ${resource}. State output: ${node}")
313 }
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){
345 for (int k=0;k<node.size();k++) {
346 def resource;
347 def resKey;
348 if(node instanceof Map){
349 resKey = node.keySet()[k]
350 }else if(node instanceof List){
351 resKey = k
352 }
353 resource = node[resKey]
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100354 if(resource instanceof Map && resource.keySet().contains("result")){
Jakub Joseff1fb4472017-03-16 14:11:54 +0100355 //clean unnesaccary fields
356 if(resource.keySet().contains("__run_num__")){
357 resource.remove("__run_num__")
358 }
359 if(resource.keySet().contains("__id__")){
360 resource.remove("__id__")
361 }
362 if(resource.keySet().contains("pchanges")){
363 resource.remove("pchanges")
364 }
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){
368 common.errorMsg(String.format("Resource: %s\n%s", resKey, prettyPrint(toJson(resource)).replace('\\n', System.getProperty('line.separator'))))
369 }else{
370 common.warningMsg(String.format("Resource: %s\n%s", resKey, prettyPrint(toJson(resource)).replace('\\n', System.getProperty('line.separator'))))
371 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100372 }else{
373 if(!onlyChanges || resource.changes.size() > 0){
374 common.successMsg(String.format("Resource: %s\n%s", resKey, prettyPrint(toJson(resource)).replace('\\n', System.getProperty('line.separator'))))
375 }
376 }
377 }else{
378 common.infoMsg(String.format("Resource: %s\n%s", resKey, prettyPrint(toJson(resource)).replace('\\n', System.getProperty('line.separator'))))
379 }
380 }
381 }else{
382 common.infoMsg(prettyPrint(toJson(node)))
383 }
384 }
Jakub Josef52f69f72017-03-14 15:18:08 +0100385 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100386 }else{
387 common.errorMsg("Salt result hasn't return attribute! Result: ${result}")
Jakub Josef79ecec32017-02-17 14:36:28 +0100388 }
Jakub Josef52f69f72017-03-14 15:18:08 +0100389 }else{
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100390 common.errorMsg("Cannot print salt state result, given result is null")
Jakub Josef79ecec32017-02-17 14:36:28 +0100391 }
392}
393
394/**
Jakub Josef7852fe12017-03-15 16:02:41 +0100395 * Print salt command run results in human-friendly form
Jakub Josef79ecec32017-02-17 14:36:28 +0100396 *
397 * @param result Parsed response of Salt API
Jakub Josef79ecec32017-02-17 14:36:28 +0100398 */
Filip Pytlound2f1bbe2017-02-27 19:03:51 +0100399def printSaltCommandResult(result) {
Jakub Josef871bf152017-03-14 20:13:41 +0100400 def common = new com.mirantis.mk.Common()
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100401 if(result != null){
402 if(result['return']){
403 for (int i=0; i<result['return'].size(); i++) {
404 def entry = result['return'][i]
405 for (int j=0; j<entry.size(); j++) {
406 common.debugMsg("printSaltCommandResult: printing salt command entry: ${entry}")
407 def nodeKey = entry.keySet()[j]
408 def node=entry[nodeKey]
409 common.infoMsg(String.format("Node %s changes:\n%s",nodeKey,prettyPrint(toJson(node)).replace('\\n', System.getProperty('line.separator'))))
410 }
Jakub Josef8a715bf2017-03-14 21:39:01 +0100411 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100412 }else{
413 common.errorMsg("Salt result hasn't return attribute! Result: ${result}")
Jakub Josef79ecec32017-02-17 14:36:28 +0100414 }
Jakub Josef8a715bf2017-03-14 21:39:01 +0100415 }else{
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100416 common.errorMsg("Cannot print salt command result, given result is null")
Jakub Josef52f69f72017-03-14 15:18:08 +0100417 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100418}