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