Migrate "ms/controllerblueprints" from ccsdk/apps
[ccsdk/cds.git] / ms / blueprintsprocessor / functions / netconf-executor / src / main / kotlin / org / onap / ccsdk / apps / blueprintsprocessor / functions / netconf / executor / utils / NetconfMessageUtils.kt
1 /*
2  * Copyright © 2017-2019 AT&T, Bell Canada
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 package org.onap.ccsdk.apps.blueprintsprocessor.functions.netconf.executor.utils
17
18 import org.apache.commons.lang3.StringUtils
19 import org.onap.ccsdk.apps.blueprintsprocessor.functions.netconf.executor.api.NetconfException
20 import org.slf4j.LoggerFactory
21 import org.xml.sax.InputSource
22 import java.io.StringReader
23 import java.nio.charset.StandardCharsets
24 import java.util.regex.MatchResult
25 import java.util.regex.Pattern
26 import javax.xml.XMLConstants
27 import javax.xml.parsers.DocumentBuilderFactory
28 import kotlin.text.Charsets.UTF_8
29
30
31 class NetconfMessageUtils {
32
33     companion object {
34         val log = LoggerFactory.getLogger(NetconfMessageUtils::class.java)
35
36         const val NEW_LINE = "\n"
37         const val CHUNKED_END_REGEX_PATTERN = "\n##\n"
38
39         val CAPABILITY_REGEX_PATTERN: Pattern = Pattern.compile(RpcMessageUtils.CAPABILITY_REGEX)
40         val SESSION_ID_REGEX_PATTERN: Pattern = Pattern.compile(RpcMessageUtils.SESSION_ID_REGEX)
41
42         private val CHUNKED_FRAMING_PATTERN: Pattern =
43             Pattern.compile("(\\n#([1-9][0-9]*)\\n(.+))+\\n##\\n", Pattern.DOTALL)
44         private val CHUNKED_SIZE_PATTERN: Pattern = Pattern.compile("\\n#([1-9][0-9]*)\\n")
45         private val MSG_ID_STRING_PATTERN = Pattern.compile("${RpcMessageUtils.MESSAGE_ID_STRING}=\"(.*?)\"")
46
47         fun getConfig(messageId: String, configType: String, filterContent: String?): String {
48             val request = StringBuilder()
49
50             request.append("<get-config>").append(NEW_LINE)
51             request.append(RpcMessageUtils.SOURCE_OPEN).append(NEW_LINE)
52             request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
53                 .append(NEW_LINE)
54             request.append(RpcMessageUtils.SOURCE_CLOSE).append(NEW_LINE)
55
56             if (!filterContent.isNullOrEmpty()) {
57                 request.append(RpcMessageUtils.SUBTREE_FILTER_OPEN).append(NEW_LINE)
58                 request.append(filterContent).append(NEW_LINE)
59                 request.append(RpcMessageUtils.SUBTREE_FILTER_CLOSE).append(NEW_LINE)
60             }
61             request.append("</get-config>").append(NEW_LINE)
62
63             return doWrappedRpc(messageId, request.toString())
64         }
65
66         fun doWrappedRpc(messageId: String, request: String): String {
67             val rpc = StringBuilder(RpcMessageUtils.XML_HEADER).append(NEW_LINE)
68             rpc.append(RpcMessageUtils.RPC_OPEN)
69             rpc.append(RpcMessageUtils.MESSAGE_ID_STRING).append(RpcMessageUtils.EQUAL)
70             rpc.append(RpcMessageUtils.QUOTE).append(messageId).append(RpcMessageUtils.QUOTE_SPACE)
71             rpc.append(RpcMessageUtils.NETCONF_BASE_NAMESPACE).append(RpcMessageUtils.CLOSE)
72                 .append(NEW_LINE)
73             rpc.append(request)
74             rpc.append(RpcMessageUtils.RPC_CLOSE)
75             // rpc.append(NEW_LINE).append(END_PATTERN);
76
77             return rpc.toString()
78         }
79
80         fun editConfig(messageId: String, configType: String, defaultOperation: String?,
81                        newConfiguration: String): String {
82
83             val request = StringBuilder()
84
85             request.append("<edit-config>").append(NEW_LINE)
86             request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
87             request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
88                 .append(NEW_LINE)
89             request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
90
91             if (defaultOperation != null) {
92                 request.append(RpcMessageUtils.DEFAULT_OPERATION_OPEN).append(defaultOperation)
93                     .append(RpcMessageUtils.DEFAULT_OPERATION_CLOSE)
94                 request.append(NEW_LINE)
95             }
96
97             request.append(RpcMessageUtils.CONFIG_OPEN).append(NEW_LINE)
98             request.append(newConfiguration.trim { it <= ' ' }).append(NEW_LINE)
99             request.append(RpcMessageUtils.CONFIG_CLOSE).append(NEW_LINE)
100             request.append("</edit-config>").append(NEW_LINE)
101
102             return doWrappedRpc(messageId, request.toString())
103         }
104
105         fun validate(messageId: String, configType: String): String {
106             val request = StringBuilder()
107
108             request.append("<validate>").append(NEW_LINE)
109             request.append(RpcMessageUtils.SOURCE_OPEN).append(NEW_LINE)
110             request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
111                 .append(NEW_LINE)
112             request.append(RpcMessageUtils.SOURCE_CLOSE).append(NEW_LINE)
113             request.append("</validate>").append(NEW_LINE)
114
115             return doWrappedRpc(messageId, request.toString())
116         }
117
118         fun commit(messageId: String, confirmed: Boolean, confirmTimeout: Int, persist: String,
119                    persistId: String): String {
120
121             if (!persist.isEmpty() && !persistId.isEmpty()) {
122                 throw NetconfException("Can't proceed <commit> with both persist($persist) and " +
123                         "persistId($persistId) specified. Only one should be specified.")
124             }
125             if (confirmed && !persistId.isEmpty()) {
126                 throw NetconfException("Can't proceed <commit> with both confirmed flag and " +
127                         "persistId($persistId) specified. Only one should be specified.")
128             }
129
130             val request = StringBuilder()
131             request.append("<commit>").append(NEW_LINE)
132             if (confirmed) {
133                 request.append("<confirmed/>")
134                 request.append("<confirm-timeout>$confirmTimeout</confirm-timeout>")
135                 if (!persist.isEmpty()) {
136                     request.append("<persist>$persist</persist>")
137                 }
138             }
139             if (!persistId.isEmpty()) {
140                 request.append("<persist-id>$persistId</persist-id>")
141             }
142             request.append("</commit>").append(NEW_LINE)
143
144             return doWrappedRpc(messageId, request.toString())
145         }
146
147         fun cancelCommit(messageId: String, persistId: String): String {
148             val request = StringBuilder()
149             request.append("<cancel-commit>").append(NEW_LINE)
150             if (!persistId.isEmpty()) {
151                 request.append("<persist-id>$persistId</persist-id>")
152             }
153             request.append("</cancel-commit>").append(NEW_LINE)
154
155             return doWrappedRpc(messageId, request.toString())
156         }
157
158         fun unlock(messageId: String, configType: String): String {
159             val request = StringBuilder()
160
161             request.append("<unlock>").append(NEW_LINE)
162             request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
163             request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
164                 .append(NEW_LINE)
165             request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
166             request.append("</unlock>").append(NEW_LINE)
167
168             return doWrappedRpc(messageId, request.toString())
169         }
170
171         @Throws(NetconfException::class)
172         fun deleteConfig(messageId: String, configType: String): String {
173             if (configType == NetconfDatastore.RUNNING.datastore) {
174                 log.warn("Target configuration for delete operation can't be \"running\" {}", configType)
175                 throw NetconfException("Target configuration for delete operation can't be running")
176             }
177
178             val request = StringBuilder()
179
180             request.append("<delete-config>").append(NEW_LINE)
181             request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
182             request.append(RpcMessageUtils.OPEN).append(configType)
183                 .append(RpcMessageUtils.TAG_CLOSE)
184                 .append(NEW_LINE)
185             request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
186             request.append("</delete-config>").append(NEW_LINE)
187
188             return doWrappedRpc(messageId, request.toString())
189         }
190
191         fun discardChanges(messageId: String): String {
192             val request = StringBuilder()
193             request.append("<discard-changes/>").append(NEW_LINE)
194             return doWrappedRpc(messageId, request.toString())
195         }
196
197         fun lock(messageId: String, configType: String): String {
198             val request = StringBuilder()
199
200             request.append("<lock>").append(NEW_LINE)
201             request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
202             request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
203                 .append(NEW_LINE)
204             request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
205             request.append("</lock>").append(NEW_LINE)
206
207             return doWrappedRpc(messageId, request.toString())
208         }
209
210         fun closeSession(messageId: String, force: Boolean): String {
211             val request = StringBuilder()
212
213             if (force) {
214                 request.append("<kill-session/>").append(NEW_LINE)
215             } else {
216                 request.append("<close-session/>").append(NEW_LINE)
217             }
218
219             return doWrappedRpc(messageId, request.toString())
220         }
221
222         fun validateRPCXML(rpcRequest: String): Boolean {
223             try {
224                 if (StringUtils.isBlank(rpcRequest)) {
225                     return false
226                 }
227                 val dbf = DocumentBuilderFactory.newInstance()
228                 dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
229                 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false)
230                 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
231                 dbf.newDocumentBuilder()
232                     .parse(InputSource(StringReader(rpcRequest.replace(RpcMessageUtils.END_PATTERN, ""))))
233                 return true
234             } catch (e: Exception) {
235                 return false
236             }
237
238         }
239
240         fun getMsgId(message: String): String {
241             val matcher = MSG_ID_STRING_PATTERN.matcher(message)
242             if (matcher.find()) {
243                 return matcher.group(1)
244             }
245             return if (message.contains(RpcMessageUtils.HELLO)) {
246                 (-1).toString()
247             } else ""
248         }
249
250         fun validateChunkedFraming(reply: String): Boolean {
251             val matcher = CHUNKED_FRAMING_PATTERN.matcher(reply)
252             if (!matcher.matches()) {
253                 log.debug("Error Reply: {}", reply)
254                 return false
255             }
256             val chunkM = CHUNKED_SIZE_PATTERN.matcher(reply)
257             val chunks = ArrayList<MatchResult>()
258             var chunkdataStr = ""
259             while (chunkM.find()) {
260                 chunks.add(chunkM.toMatchResult())
261                 // extract chunk-data (and later) in bytes
262                 val bytes = Integer.parseInt(chunkM.group(1))
263                 val chunkdata = reply.substring(chunkM.end()).toByteArray(StandardCharsets.UTF_8)
264                 if (bytes > chunkdata.size) {
265                     log.debug("Error Reply - wrong chunk size {}", reply)
266                     return false
267                 }
268                 // convert (only) chunk-data part into String
269                 chunkdataStr = String(chunkdata, 0, bytes, StandardCharsets.UTF_8)
270                 // skip chunk-data part from next match
271                 chunkM.region(chunkM.end() + chunkdataStr.length, reply.length)
272             }
273             if (!CHUNKED_END_REGEX_PATTERN.equals(reply.substring(chunks[chunks.size - 1].end() + chunkdataStr.length))) {
274                 log.debug("Error Reply: {}", reply)
275                 return false
276             }
277             return true
278         }
279
280         fun createHelloString(capabilities: List<String>): String {
281             val helloMessage = StringBuilder()
282             helloMessage.append(RpcMessageUtils.XML_HEADER).append(NEW_LINE)
283             helloMessage.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">").append(NEW_LINE)
284             helloMessage.append("  <capabilities>").append(NEW_LINE)
285             if (capabilities.isNotEmpty()) {
286                 capabilities.forEach { cap ->
287                     helloMessage.append("    <capability>").append(cap).append("</capability>").append(NEW_LINE)
288                 }
289             }
290             helloMessage.append("  </capabilities>").append(NEW_LINE)
291             helloMessage.append("</hello>").append(NEW_LINE)
292             helloMessage.append(RpcMessageUtils.END_PATTERN)
293             return helloMessage.toString()
294         }
295
296         fun formatRPCRequest(request: String, messageId: String, deviceCapabilities: Set<String>): String {
297             var request = request
298             request = NetconfMessageUtils.formatNetconfMessage(deviceCapabilities, request)
299             request = NetconfMessageUtils.formatXmlHeader(request)
300             request = NetconfMessageUtils.formatRequestMessageId(request, messageId)
301
302             return request
303         }
304
305         /**
306          * Validate and format netconf message. - NC1.0 if no EOM sequence present on `message`,
307          * append. - NC1.1 chunk-encode given message unless it already is chunk encoded
308          *
309          * @param deviceCapabilities Set containing Device Capabilities
310          * @param message to format
311          * @return formated message
312          */
313         fun formatNetconfMessage(deviceCapabilities: Set<String>, message: String): String {
314             var message = message
315             if (deviceCapabilities.contains(RpcMessageUtils.NETCONF_11_CAPABILITY)) {
316                 message = formatChunkedMessage(message)
317             } else if (!message.endsWith(RpcMessageUtils.END_PATTERN)) {
318                 message = message + NEW_LINE + RpcMessageUtils.END_PATTERN
319             }
320             return message
321         }
322
323         /**
324          * Validate and format message according to chunked framing mechanism.
325          *
326          * @param message to format
327          * @return formated message
328          */
329         fun formatChunkedMessage(message: String): String {
330             var message = message
331             if (message.endsWith(RpcMessageUtils.END_PATTERN)) {
332                 // message given had Netconf 1.0 EOM pattern -> remove
333                 message = message.substring(0, message.length - RpcMessageUtils.END_PATTERN.length)
334             }
335             if (!message.startsWith(RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH)) {
336                 // chunk encode message
337                 message =
338                     (RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH + message.toByteArray(UTF_8).size + RpcMessageUtils.NEW_LINE + message + RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH
339                             + RpcMessageUtils.NEW_LINE)
340             }
341             return message
342         }
343
344         /**
345          * Ensures xml start directive/declaration appears in the `request`.
346          *
347          * @param request RPC request message
348          * @return XML RPC message
349          */
350         fun formatXmlHeader(request: String): String {
351             var request = request
352             if (!request.contains(RpcMessageUtils.XML_HEADER)) {
353                 if (request.startsWith(RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH)) {
354                     request =
355                         request.split("<".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + RpcMessageUtils.XML_HEADER + request.substring(
356                             request.split("<".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0].length)
357                 } else {
358                     request = RpcMessageUtils.XML_HEADER + "\n" + request
359                 }
360             }
361             return request
362         }
363
364         fun formatRequestMessageId(request: String, messageId: String): String {
365             var request = request
366             if (request.contains(RpcMessageUtils.MESSAGE_ID_STRING)) {
367                 request =
368                     request.replaceFirst((RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.NUMBER_BETWEEN_QUOTES_MATCHER).toRegex(),
369                         RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.QUOTE + messageId + RpcMessageUtils.QUOTE)
370             } else if (!request.contains(RpcMessageUtils.MESSAGE_ID_STRING) && !request.contains(
371                     RpcMessageUtils.HELLO)) {
372                 request = request.replaceFirst(RpcMessageUtils.END_OF_RPC_OPEN_TAG.toRegex(),
373                     RpcMessageUtils.QUOTE_SPACE + RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.QUOTE + messageId + RpcMessageUtils.QUOTE + ">")
374             }
375             return updateRequestLength(request)
376         }
377
378         fun updateRequestLength(request: String): String {
379             if (request.contains(NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH + NEW_LINE)) {
380                 val oldLen =
381                     Integer.parseInt(request.split(RpcMessageUtils.HASH.toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1].split(
382                         NEW_LINE.toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[0])
383                 val rpcWithEnding = request.substring(request.indexOf('<'))
384                 val firstBlock =
385                     request.split(RpcMessageUtils.MSGLEN_REGEX_PATTERN.toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1].split(
386                         (NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH + NEW_LINE).toRegex()).dropLastWhile(
387                         { it.isEmpty() }).toTypedArray()[0]
388                 var newLen = 0
389                 newLen = firstBlock.toByteArray(UTF_8).size
390                 if (oldLen != newLen) {
391                     return NEW_LINE + RpcMessageUtils.HASH + newLen + NEW_LINE + rpcWithEnding
392                 }
393             }
394             return request
395         }
396
397         fun checkReply(reply: String?): Boolean {
398             return if (reply != null) {
399                 !reply.contains("rpc-error>") || reply.contains("warning") || reply.contains("ok/>")
400             } else false
401         }
402     }
403
404 }