6e8cdc24312ac9bba7b221e9c83a1f6d3ef98e6d
[ccsdk/cds.git] /
1 /*
2  *  Copyright © 2019 IBM.
3  *
4  *  Licensed under the Apache License, Version 2.0 (the "License");
5  *  you may not use this file except in compliance with the License.
6  *  You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *  Unless required by applicable law or agreed to in writing, software
11  *  distributed under the License is distributed on an "AS IS" BASIS,
12  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  *  See the License for the specific language governing permissions and
14  *  limitations under the License.
15  */
16
17 package org.onap.ccsdk.cds.blueprintsprocessor.functions.ansible.executor
18
19 import com.fasterxml.jackson.databind.JsonNode
20 import com.fasterxml.jackson.databind.ObjectMapper
21 import com.fasterxml.jackson.databind.node.ObjectNode
22 import java.net.URLEncoder
23 import java.util.NoSuchElementException
24 import org.onap.ccsdk.cds.blueprintsprocessor.core.api.data.*
25 import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BluePrintRestLibPropertyService
26 import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService
27 import org.onap.ccsdk.cds.blueprintsprocessor.services.execution.AbstractComponentFunction
28 import org.onap.ccsdk.cds.controllerblueprints.core.*
29 import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils
30 import org.slf4j.LoggerFactory
31 import org.springframework.beans.factory.config.ConfigurableBeanFactory
32 import org.springframework.context.annotation.Scope
33 import org.springframework.http.HttpMethod
34 import org.springframework.stereotype.Component
35
36 /**
37  * ComponentRemoteAnsibleExecutor
38  *
39  * Component that launches a run of a job template (INPUT_JOB_TEMPLATE_NAME) representing an Ansible playbook,
40  * and its parameters, via the AWX server identified by the INPUT_ENDPOINT_SELECTOR parameter.
41  *
42  * It supports extra_vars, limit, tags, skip-tags, inventory (by name or Id) Ansible parameters.
43  * It reports the results of the execution via properties, named execute-command-status and execute-command-logs
44  *
45  * @author Serge Simard
46  */
47 @Component("component-remote-ansible-executor")
48 @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
49 open class ComponentRemoteAnsibleExecutor(private val blueprintRestLibPropertyService: BluePrintRestLibPropertyService)
50     : AbstractComponentFunction() {
51
52     private val log = LoggerFactory.getLogger(ComponentRemoteAnsibleExecutor::class.java)!!
53
54     // HTTP related constants
55     private val HTTP_SUCCESS = 200..202
56     private val GET = HttpMethod.GET.name
57     private val POST = HttpMethod.POST.name
58
59     companion object {
60         // input fields names accepted by this executor
61         const val INPUT_ENDPOINT_SELECTOR = "endpoint-selector"
62         const val INPUT_JOB_TEMPLATE_NAME = "job-template-name"
63         const val INPUT_LIMIT_TO_HOST = "limit"
64         const val INPUT_INVENTORY = "inventory"
65         const val INPUT_EXTRA_VARS = "extra-vars"
66         const val INPUT_TAGS = "tags"
67         const val INPUT_SKIP_TAGS = "skip-tags"
68
69         // output fields names populated by this executor
70         const val ATTRIBUTE_EXEC_CMD_STATUS = "ansible-command-status"
71         const val ATTRIBUTE_EXEC_CMD_LOG = "ansible-command-logs"
72
73         const val CHECKDELAY: Long = 10000
74     }
75
76     override suspend fun processNB(executionRequest: ExecutionServiceInput) {
77
78         try {
79             val restClientService = getAWXRestClient()
80
81             val jobTemplateName = getOperationInput(INPUT_JOB_TEMPLATE_NAME).asText()
82             val jtId = lookupJobTemplateIDByName(restClientService, jobTemplateName)
83             if (jtId.isNotEmpty()) {
84                 runJobTemplateOnAWX(restClientService, jobTemplateName, jtId)
85             } else {
86                 val message = "Job template ${jobTemplateName} does not exists"
87                 log.error(message)
88                 setNodeOutputErrors("Failed", message)
89             }
90         } catch (e: Exception) {
91             log.error("Failed to process on remote executor (${e.message})", e)
92             setNodeOutputErrors("Failed", "Failed to process on remote executor (${e.message})")
93         }
94     }
95
96
97     override suspend fun recoverNB(runtimeException: RuntimeException, executionRequest: ExecutionServiceInput) {
98         val message = "Error in ComponentRemoteAnsibleExecutor : ${runtimeException.message}"
99         log.error(message,runtimeException)
100         setNodeOutputErrors("Failed", message)
101     }
102
103     /** Creates a TokenAuthRestClientService, since this executor expect type property to be "token-auth" and the
104      * token to be an OAuth token (access_token response field) generated via the AWX /api/o/token rest endpoint
105      * The token field is of the form "Bearer access_token_from_response", for example :
106      *  "blueprintsprocessor.restclient.awx.type=token-auth"
107      *  "blueprintsprocessor.restclient.awx.url=http://ipaddress"
108      *  "blueprintsprocessor.restclient.awx.token=Bearer J9gEtMDqf7P4YsJ74fioY9VAhLDIs1"
109      */
110     private fun getAWXRestClient(): BlueprintWebClientService {
111
112         val endpointSelector = getOperationInput(INPUT_ENDPOINT_SELECTOR).asText()// TODO not asText
113
114         try {
115             return blueprintRestLibPropertyService.blueprintWebClientService(endpointSelector)
116         } catch (e : NoSuchElementException) {
117             throw IllegalArgumentException("No value provided for input selector $endpointSelector", e)
118         }
119     }
120
121     /**
122      * Finds the job template ID based on the job template name provided in the request
123      */
124     private fun lookupJobTemplateIDByName(awxClient : BlueprintWebClientService, job_template_name: String?): String {
125         val mapper = ObjectMapper()
126
127         // Get Job Template details by name
128         var response = awxClient.exchangeResource(GET, "/api/v2/job_templates/${job_template_name}/", "")
129         val jtDetails: JsonNode = mapper.readTree(response.body)
130         return jtDetails.at("/id").asText()
131     }
132
133     /**
134      * Performs the job template execution on AWX, ie. prepare arguments as per job template
135      * requirements (ask fields) and provided overriding values. Then it launches the run, and monitors
136      * its execution. Finally, it retrieves the job results via the stdout api.
137      * The status and output attributes are populated in the process.
138      */
139     private fun runJobTemplateOnAWX(awxClient : BlueprintWebClientService, job_template_name: String?, jtId: String) {
140         val mapper = ObjectMapper()
141
142         setNodeOutputProperties( "preparing".asJsonPrimitive(), "".asJsonPrimitive())
143
144         // Get Job Template requirements
145         var response = awxClient.exchangeResource(GET, "/api/v2/job_templates/${jtId}/launch/","")
146         val jtLaunchReqs: JsonNode = mapper.readTree(response.body)
147         var payload = prepareLaunchPayload(awxClient, jtLaunchReqs)
148         log.info("Running job with $payload, for requestId $processId.")
149
150         // Launch the job for the targeted template
151         var jtLaunched : JsonNode = JacksonUtils.jsonNode("{}") as ObjectNode
152         response = awxClient.exchangeResource(POST, "/api/v2/job_templates/${jtId}/launch/", payload)
153         if (response.status in HTTP_SUCCESS) {
154             jtLaunched = mapper.readTree(response.body)
155             val fieldsIgnored: JsonNode = jtLaunched.at("/ignored_fields")
156             if (fieldsIgnored.rootFieldsToMap().isNotEmpty()) {
157                 log.warn("Ignored fields : $fieldsIgnored, for requestId $processId.")
158             }
159         }
160
161         if (response.status in HTTP_SUCCESS) {
162             val jobId: String = jtLaunched.at("/id").asText()
163
164             // Poll current job status while job is not executed
165             var jobStatus = "unknown"
166             var jobEndTime = "null"
167             while (jobEndTime == "null") {
168                 response = awxClient.exchangeResource(GET, "/api/v2/jobs/${jobId}/", "")
169                 val jobLaunched: JsonNode = mapper.readTree(response.body)
170                 jobStatus = jobLaunched.at("/status").asText()
171                 jobEndTime = jobLaunched.at("/finished").asText()
172                 Thread.sleep(CHECKDELAY)
173             }
174
175             log.info("Execution of job template $job_template_name in job #$jobId finished with status ($jobStatus) for requestId $processId")
176
177             // Get job execution results (stdout)
178             val plainTextHeaders = mutableMapOf<String, String>()
179             plainTextHeaders["Content-Type"] = "text/plain ;utf-8"
180             response = awxClient.exchangeResource(GET, "/api/v2/jobs/${jobId}/stdout/?format=txt","", plainTextHeaders)
181
182             setNodeOutputProperties( jobStatus.asJsonPrimitive(), response.body.asJsonPrimitive())
183         } else {
184             // The job template requirements were not fulfilled with the values passed in. The message below will
185             // provide more information via the response, like the ignored_fields, or variables_needed_to_start,
186             // or resources_needed_to_start, in order to help user pinpoint the problems with the request.
187             val message = "Execution of job template $job_template_name could not be started for requestId $processId." +
188                     " (Response: ${response.body}) "
189             log.error(message)
190             setNodeOutputErrors( response.status.toString(), message)
191         }
192     }
193
194     /**
195      * Prepares the JSON payload expected by the job template api,
196      * by applying the overrides that were provided
197      * and allowed by the template definition flags in jtLaunchReqs
198      */
199     private fun prepareLaunchPayload(awxClient : BlueprintWebClientService, jtLaunchReqs: JsonNode): String {
200         val payload = JacksonUtils.jsonNode("{}") as ObjectNode
201
202         // Parameter defaults
203         val limitProp = getOptionalOperationInput(INPUT_LIMIT_TO_HOST)?.asText()
204         val tagsProp = getOptionalOperationInput(INPUT_TAGS)?.asText()
205         val skipTagsProp = getOptionalOperationInput(INPUT_SKIP_TAGS)?.asText()
206         val inventoryProp : String? = getOptionalOperationInput(INPUT_INVENTORY)?.asText()
207         val extraArgs : JsonNode = getOperationInput(INPUT_EXTRA_VARS)
208
209         val askLimitOnLaunch = jtLaunchReqs.at( "/ask_limit_on_launch").asBoolean()
210         if (askLimitOnLaunch && limitProp!!.isNotEmpty()) {
211             payload.put(INPUT_LIMIT_TO_HOST, limitProp)
212         }
213         val askTagsOnLaunch = jtLaunchReqs.at("/ask_tags_on_launch").asBoolean()
214         if (askTagsOnLaunch && tagsProp!!.isNotEmpty()) {
215             payload.put(INPUT_TAGS, tagsProp)
216         }
217         if (askTagsOnLaunch && skipTagsProp!!.isNotEmpty()) {
218             payload.put("skip_tags", skipTagsProp)
219         }
220         val askInventoryOnLaunch = jtLaunchReqs.at("/ask_inventory_on_launch").asBoolean()
221         if (askInventoryOnLaunch && inventoryProp != null) {
222             var inventoryKeyId = inventoryProp.toIntOrNull()
223             if (inventoryKeyId == null) {
224                 inventoryKeyId = resolveInventoryIdByName(awxClient, inventoryProp)
225             }
226             payload.put(INPUT_INVENTORY, inventoryKeyId)
227         }
228         val askVariablesOnLaunch = jtLaunchReqs.at("/ask_variables_on_launch").asBoolean()
229         if (askVariablesOnLaunch && extraArgs != null) {
230             payload.put("extra_vars", extraArgs)
231         }
232
233         val strPayload = "$payload"
234
235         return strPayload
236     }
237
238     private fun resolveInventoryIdByName(awxClient : BlueprintWebClientService, inventoryProp: String): Int? {
239         var invId : Int? = null
240
241         // Get Inventory by name
242         val encoded = URLEncoder.encode(inventoryProp)
243         val response = awxClient.exchangeResource(GET,"/api/v2/inventories/?name=$encoded","")
244                 //, getRequestHeaders("awx"))
245         if (response.status in HTTP_SUCCESS) {
246             val mapper = ObjectMapper()
247
248             // Extract the inventory ID from response
249             val invDetails = mapper.readTree(response.body)
250             val nbInvFound = invDetails.at("/count").asInt()
251             if (nbInvFound == 1) {
252                 invId = invDetails["results"][0]["id"].asInt()
253             }
254             if (invId == null) {
255                 log.error("Could not resolve inventory $inventoryProp by name...")
256             } else {
257                 log.info("Resolved inventory $inventoryProp to ID #: $invId")
258             }
259         }
260         return invId
261     }
262
263     /**
264      * Utility function to set the output properties of the executor node
265      */
266     private fun setNodeOutputProperties(status: JsonNode, message: JsonNode) {
267         setAttribute(ATTRIBUTE_EXEC_CMD_STATUS, status)
268         log.info("Executor status: $status")
269         setAttribute(ATTRIBUTE_EXEC_CMD_LOG, message)
270         log.info("Executor message: $message")
271     }
272
273     /**
274      * Utility function to set the output properties and errors of the executor node, in cas of errors
275      */
276     private fun setNodeOutputErrors(status: String, message: String) {
277         setAttribute(ATTRIBUTE_EXEC_CMD_STATUS, status.asJsonPrimitive())
278         setAttribute(ATTRIBUTE_EXEC_CMD_LOG, message.asJsonPrimitive())
279
280         addError("error", ATTRIBUTE_EXEC_CMD_LOG, "$status : $message")
281     }
282 }