blob: 65c9902a553d83b9a4a5eac214572b685b20dced [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 Josef79ecec32017-02-17 14:36:28 +0100141 try {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100142 checkResult(out, failOnError)
Jakub Josef79ecec32017-02-17 14:36:28 +0100143 } finally {
144 if (output == true) {
Filip Pytlound2f1bbe2017-02-27 19:03:51 +0100145 printSaltStateResult(out)
Jakub Josef79ecec32017-02-17 14:36:28 +0100146 }
147 }
148 return out
149}
150
Jakub Josef5ade54c2017-03-10 16:14:01 +0100151/**
152 * Run command on salt minion (salt cmd.run wrapper)
153 * @param master Salt connection object
154 * @param target Get pillar target
155 * @param cmd command
156 * @return output of salt command
157 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100158def cmdRun(master, target, cmd) {
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100159 def common = new com.mirantis.mk.Common()
160
Tomáš Kukráldfd4b492017-03-02 12:08:50 +0100161 common.infoMsg("Running command ${cmd} on ${target}")
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100162
Jakub Josef5ade54c2017-03-10 16:14:01 +0100163 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'cmd.run', null, [cmd])
Jakub Josef79ecec32017-02-17 14:36:28 +0100164}
165
Jakub Josef5ade54c2017-03-10 16:14:01 +0100166/**
167 * Perform complete salt sync between master and target
168 * @param master Salt connection object
169 * @param target Get pillar target
170 * @return output of salt command
171 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100172def syncAll(master, target) {
Filip Pytloun5a7f7fd2017-02-27 18:50:25 +0100173 return runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'saltutil.sync_all')
Jakub Josef79ecec32017-02-17 14:36:28 +0100174}
175
Jakub Josef5ade54c2017-03-10 16:14:01 +0100176/**
177 * Enforce highstate on given targets
178 * @param master Salt connection object
179 * @param target Highstate enforcing target
180 * @param output print output (optional, default true)
181 * @param failOnError throw exception on salt state result:false (optional, default true)
182 * @return output of salt command
183 */
184def enforceHighstate(master, target, output = false, failOnError = true) {
Filip Pytloun5a7f7fd2017-02-27 18:50:25 +0100185 def out = runSaltCommand(master, 'local', ['expression': target, 'type': 'compound'], 'state.highstate')
Jakub Josef79ecec32017-02-17 14:36:28 +0100186 try {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100187 checkResult(out, failOnError)
Jakub Josef79ecec32017-02-17 14:36:28 +0100188 } finally {
189 if (output == true) {
Filip Pytlound2f1bbe2017-02-27 19:03:51 +0100190 printSaltStateResult(out)
Jakub Josef79ecec32017-02-17 14:36:28 +0100191 }
192 }
193 return out
194}
195
Jakub Josef5ade54c2017-03-10 16:14:01 +0100196/**
197 * Generates node key using key.gen_accept call
198 * @param master Salt connection object
199 * @param target Key generating target
200 * @param host Key generating host
201 * @param keysize generated key size (optional, default 4096)
202 * @return output of salt command
203 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100204def generateNodeKey(master, target, host, keysize = 4096) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100205 return runSaltCommand(master, 'wheel', target, 'key.gen_accept', [host], ['keysize': keysize])
Jakub Josef79ecec32017-02-17 14:36:28 +0100206}
207
Jakub Josef5ade54c2017-03-10 16:14:01 +0100208/**
209 * Generates node reclass metadata
210 * @param master Salt connection object
211 * @param target Metadata generating target
212 * @param host Metadata generating host
213 * @param classes Reclass classes
214 * @param parameters Reclass parameters
215 * @return output of salt command
216 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100217def generateNodeMetadata(master, target, host, classes, parameters) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100218 return runSaltCommand(master, 'local', target, 'reclass.node_create', [host, '_generated'], ['classes': classes, 'parameters': parameters])
Jakub Josef79ecec32017-02-17 14:36:28 +0100219}
220
Jakub Josef5ade54c2017-03-10 16:14:01 +0100221/**
222 * Run salt orchestrate on given targets
223 * @param master Salt connection object
224 * @param target Orchestration target
225 * @param orchestrate Salt orchestrate params
226 * @return output of salt command
227 */
Jakub Josef79ecec32017-02-17 14:36:28 +0100228def orchestrateSystem(master, target, orchestrate) {
229 return runSaltCommand(master, 'runner', target, 'state.orchestrate', [orchestrate])
230}
231
Jakub Josef5ade54c2017-03-10 16:14:01 +0100232/**
233 * Run salt process step
234 * @param master Salt connection object
235 * @param tgt Salt process step target
236 * @param fun Salt process step function
237 * @param arg process step arguments (optional, default [])
238 * @param batch using batch (optional, default false)
239 * @param output print output (optional, default false)
240 * @return output of salt command
241 */
Tomáš Kukrálf5dda642017-03-02 14:22:59 +0100242def runSaltProcessStep(master, tgt, fun, arg = [], batch = null, output = false) {
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100243 def common = new com.mirantis.mk.Common()
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100244 def out
245
Tomas Kukrale90bb342017-03-02 21:30:35 +0000246 common.infoMsg("Running step ${fun} on ${tgt}")
Tomáš Kukrál6c04bd02017-03-01 22:18:52 +0100247
Filip Pytlounf0435c02017-03-02 17:48:54 +0100248 if (batch == true) {
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100249 out = runSaltCommand(master, 'local_batch', ['expression': tgt, 'type': 'compound'], fun, String.valueOf(batch), arg)
250 } else {
251 out = runSaltCommand(master, 'local', ['expression': tgt, 'type': 'compound'], fun, batch, arg)
Jakub Josef79ecec32017-02-17 14:36:28 +0100252 }
Tomáš Kukráladb4ecd2017-03-02 10:06:36 +0100253
Tomáš Kukrálf5dda642017-03-02 14:22:59 +0100254 if (output == true) {
Jakub Josef5ade54c2017-03-10 16:14:01 +0100255 printSaltCommandResult(out)
Jakub Josef79ecec32017-02-17 14:36:28 +0100256 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100257}
258
259/**
260 * Check result for errors and throw exception if any found
261 *
262 * @param result Parsed response of Salt API
Jakub Josef5ade54c2017-03-10 16:14:01 +0100263 * @param failOnError Do you want to throw exception if salt-call fails
Jakub Josef79ecec32017-02-17 14:36:28 +0100264 */
Jakub Josef5ade54c2017-03-10 16:14:01 +0100265def checkResult(result, failOnError = true) {
266 def common = new com.mirantis.mk.Common()
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100267 if(result != null){
268 if(result['return']){
269 for (int i=0;i<result['return'].size();i++) {
270 def entry = result['return'][i]
271 if (!entry) {
272 if (failOnError) {
273 throw new Exception("Salt API returned empty response: ${result}")
274 } else {
275 common.errorMsg("Salt API returned empty response: ${result}")
Jakub Josefece32af2017-03-14 19:20:08 +0100276 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100277 }
278 for (int j=0;j<entry.size();j++) {
279 def nodeKey = entry.keySet()[j]
280 def node=entry[nodeKey]
281 for (int k=0;k<node.size();k++) {
282 def resource;
283 def resKey;
284 if(node instanceof Map){
285 resKey = node.keySet()[k]
286 }else if(node instanceof List){
287 resKey = k
288 }
289 resource = node[resKey]
290 common.debugMsg("checkResult: checking resource: ${resource}")
291 if(resource instanceof String || !resource["result"] || (resource["result"] instanceof String && resource["result"] != "true")){
292 if (failOnError) {
293 throw new Exception("Salt state on node ${nodeKey} failed: ${resource}. State output: ${node}")
294 } else {
295 common.errorMsg("Salt state on node ${nodeKey} failed: ${resource}. State output: ${node}")
296 }
Jakub Josef52f69f72017-03-14 15:18:08 +0100297 }
Jakub Josef5ade54c2017-03-10 16:14:01 +0100298 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100299 }
300 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100301 }else{
302 common.errorMsg("Salt result hasn't return attribute! Result: ${result}")
Jakub Josef79ecec32017-02-17 14:36:28 +0100303 }
Jakub Josef52f69f72017-03-14 15:18:08 +0100304 }else{
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100305 common.errorMsg("Cannot check salt result, given result is null")
Jakub Josef79ecec32017-02-17 14:36:28 +0100306 }
307}
308
309/**
310 * Print Salt state run results in human-friendly form
311 *
312 * @param result Parsed response of Salt API
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100313 * @param onlyChanges If true (default), print only changed resources
Jakub Josef79ecec32017-02-17 14:36:28 +0100314 */
315def printSaltStateResult(result, onlyChanges = true) {
Tomáš Kukrál6eb45f82017-03-08 18:26:16 +0100316 def common = new com.mirantis.mk.Common()
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100317 if(result != null){
318 if(result['return']){
319 for (int i=0; i<result['return'].size(); i++) {
320 def entry = result['return'][i]
321 for (int j=0; j<entry.size(); j++) {
322 common.debugMsg("printSaltStateResult: printing salt command entry: ${entry}")
323 def nodeKey = entry.keySet()[j]
324 def node=entry[nodeKey]
325 common.infoMsg("Node ${nodeKey} changes:")
326 if(node instanceof Map || node instanceof List){
327 for (int k=0;k<node.size();k++) {
328 def resource;
329 def resKey;
330 if(node instanceof Map){
331 resKey = node.keySet()[k]
332 }else if(node instanceof List){
333 resKey = k
334 }
335 resource = node[resKey]
336 //clean unnesaccary fields
337 resource.remove("__run_num__")
338 resource.remove("__id__")
339 if(resource instanceof Map && resource.keySet().contains("result")){
340 if(!resource["result"] || (resource["result"] instanceof String && resource["result"] != "true")){
341 common.errorMsg(String.format("Resource: %s\n%s", resKey, prettyPrint(toJson(resource)).replace('\\n', System.getProperty('line.separator'))))
342 }else{
343 if(!onlyChanges || resource.changes.size() > 0){
344 common.successMsg(String.format("Resource: %s\n%s", resKey, prettyPrint(toJson(resource)).replace('\\n', System.getProperty('line.separator'))))
345 }
346 }
347 }else{
348 common.infoMsg(String.format("Resource: %s\n%s", resKey, prettyPrint(toJson(resource)).replace('\\n', System.getProperty('line.separator'))))
349 }
350 }
351 }else{
352 common.infoMsg(prettyPrint(toJson(node)))
353 }
354 }
Jakub Josef52f69f72017-03-14 15:18:08 +0100355 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100356 }else{
357 common.errorMsg("Salt result hasn't return attribute! Result: ${result}")
Jakub Josef79ecec32017-02-17 14:36:28 +0100358 }
Jakub Josef52f69f72017-03-14 15:18:08 +0100359 }else{
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100360 common.errorMsg("Cannot print salt state result, given result is null")
Jakub Josef79ecec32017-02-17 14:36:28 +0100361 }
362}
363
364/**
Jakub Josef7852fe12017-03-15 16:02:41 +0100365 * Print salt command run results in human-friendly form
Jakub Josef79ecec32017-02-17 14:36:28 +0100366 *
367 * @param result Parsed response of Salt API
Jakub Josef79ecec32017-02-17 14:36:28 +0100368 */
Filip Pytlound2f1bbe2017-02-27 19:03:51 +0100369def printSaltCommandResult(result) {
Jakub Josef871bf152017-03-14 20:13:41 +0100370 def common = new com.mirantis.mk.Common()
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100371 if(result != null){
372 if(result['return']){
373 for (int i=0; i<result['return'].size(); i++) {
374 def entry = result['return'][i]
375 for (int j=0; j<entry.size(); j++) {
376 common.debugMsg("printSaltCommandResult: printing salt command entry: ${entry}")
377 def nodeKey = entry.keySet()[j]
378 def node=entry[nodeKey]
379 common.infoMsg(String.format("Node %s changes:\n%s",nodeKey,prettyPrint(toJson(node)).replace('\\n', System.getProperty('line.separator'))))
380 }
Jakub Josef8a715bf2017-03-14 21:39:01 +0100381 }
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100382 }else{
383 common.errorMsg("Salt result hasn't return attribute! Result: ${result}")
Jakub Josef79ecec32017-02-17 14:36:28 +0100384 }
Jakub Josef8a715bf2017-03-14 21:39:01 +0100385 }else{
Jakub Josefd9afd0e2017-03-15 19:19:23 +0100386 common.errorMsg("Cannot print salt command result, given result is null")
Jakub Josef52f69f72017-03-14 15:18:08 +0100387 }
Jakub Josef79ecec32017-02-17 14:36:28 +0100388}