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