2 * Copyright © 2017-2019 AT&T, Bell Canada
3 * Modifications Copyright (c) 2019 IBM.
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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.utils
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
32 class NetconfMessageUtils {
35 val log = LoggerFactory.getLogger(NetconfMessageUtils::class.java)
37 const val NEW_LINE = "\n"
38 const val CHUNKED_END_REGEX_PATTERN = "\n##\n"
40 val CAPABILITY_REGEX_PATTERN: Pattern = Pattern.compile(RpcMessageUtils.CAPABILITY_REGEX)
41 val SESSION_ID_REGEX_PATTERN: Pattern = Pattern.compile(RpcMessageUtils.SESSION_ID_REGEX)
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}=\"(.*?)\"")
48 fun get(messageId: String, filterContent: String): String {
49 val request = StringBuilder()
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)
57 request.append("</get>")
59 return doWrappedRpc(messageId, request.toString())
62 fun getConfig(messageId: String, configType: String, filterContent: String?): String {
63 val request = StringBuilder()
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)
69 request.append(RpcMessageUtils.SOURCE_CLOSE).append(NEW_LINE)
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)
76 request.append("</get-config>")
78 return doWrappedRpc(messageId, request.toString())
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 rpc.append(NEW_LINE).append(request).append(NEW_LINE)
88 rpc.append(RpcMessageUtils.RPC_CLOSE)
89 // rpc.append(NEW_LINE).append(END_PATTERN);
94 fun editConfig(messageId: String, configType: String, defaultOperation: String?,
95 newConfiguration: String): String {
96 val request = StringBuilder()
97 request.append("<edit-config>").append(NEW_LINE)
98 request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
99 request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
101 request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
103 if (defaultOperation != null) {
104 request.append(RpcMessageUtils.DEFAULT_OPERATION_OPEN).append(defaultOperation)
105 .append(RpcMessageUtils.DEFAULT_OPERATION_CLOSE)
106 request.append(NEW_LINE)
109 request.append(RpcMessageUtils.CONFIG_OPEN).append(NEW_LINE)
110 request.append(newConfiguration.trim { it <= ' ' }).append(NEW_LINE)
111 request.append(RpcMessageUtils.CONFIG_CLOSE).append(NEW_LINE)
112 request.append("</edit-config>")
114 return doWrappedRpc(messageId, request.toString())
117 fun validate(messageId: String, configType: String): String {
118 val request = StringBuilder()
120 request.append("<validate>").append(NEW_LINE)
121 request.append(RpcMessageUtils.SOURCE_OPEN).append(NEW_LINE)
122 request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
124 request.append(RpcMessageUtils.SOURCE_CLOSE).append(NEW_LINE)
125 request.append("</validate>")
127 return doWrappedRpc(messageId, request.toString())
130 fun commit(messageId: String, confirmed: Boolean, confirmTimeout: Int, persist: String,
131 persistId: String): String {
133 if (!persist.isEmpty() && !persistId.isEmpty()) {
134 throw NetconfException("Can't proceed <commit> with both persist($persist) and " +
135 "persistId($persistId) specified. Only one should be specified.")
137 if (confirmed && !persistId.isEmpty()) {
138 throw NetconfException("Can't proceed <commit> with both confirmed flag and " +
139 "persistId($persistId) specified. Only one should be specified.")
142 val request = StringBuilder()
143 request.append("<commit>").append(NEW_LINE)
145 request.append("<confirmed/>").append(NEW_LINE)
146 request.append("<confirm-timeout>$confirmTimeout</confirm-timeout>").append(NEW_LINE)
147 if (!persist.isEmpty()) {
148 request.append("<persist>$persist</persist>").append(NEW_LINE)
151 if (!persistId.isEmpty()) {
152 request.append("<persist-id>$persistId</persist-id>").append(NEW_LINE)
154 request.append("</commit>")
156 return doWrappedRpc(messageId, request.toString())
159 fun cancelCommit(messageId: String, persistId: String): String {
160 val request = StringBuilder()
161 request.append("<cancel-commit>").append(NEW_LINE)
162 if (!persistId.isEmpty()) {
163 request.append("<persist-id>$persistId</persist-id>").append(NEW_LINE)
165 request.append("</cancel-commit>")
167 return doWrappedRpc(messageId, request.toString())
170 fun unlock(messageId: String, configType: String): String {
171 val request = StringBuilder()
173 request.append("<unlock>").append(NEW_LINE)
174 request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
175 request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
177 request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
178 request.append("</unlock>")
180 return doWrappedRpc(messageId, request.toString())
183 @Throws(NetconfException::class)
184 fun deleteConfig(messageId: String, configType: String): String {
185 if (configType == NetconfDatastore.RUNNING.datastore) {
186 log.warn("Target configuration for delete operation can't be \"running\" {}", configType)
187 throw NetconfException("Target configuration for delete operation can't be running")
190 val request = StringBuilder()
192 request.append("<delete-config>").append(NEW_LINE)
193 request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
194 request.append(RpcMessageUtils.OPEN).append(configType)
195 .append(RpcMessageUtils.TAG_CLOSE)
197 request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
198 request.append("</delete-config>")
200 return doWrappedRpc(messageId, request.toString())
203 fun discardChanges(messageId: String): String {
204 val request = StringBuilder()
205 request.append("<discard-changes/>")
206 return doWrappedRpc(messageId, request.toString())
209 fun lock(messageId: String, configType: String): String {
210 val request = StringBuilder()
212 request.append("<lock>").append(NEW_LINE)
213 request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
214 request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
216 request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
217 request.append("</lock>")
219 return doWrappedRpc(messageId, request.toString())
222 fun closeSession(messageId: String, force: Boolean): String {
223 val request = StringBuilder()
224 //TODO: kill-session without session-id is a cisco-only variant.
225 //will fail on JUNIPER device.
226 //netconf RFC for kill-session requires session-id
227 //Cisco can accept <kill-session/> for current session
228 //or <kill-session><session-id>####</session-id></kill-session>
229 //as long as session ID is not the same as the current session.
231 //Juniperhttps://www.juniper.net/documentation/en_US/junos/topics/task/operational/netconf-session-terminating.html
232 //will accept only with session-id
234 request.append("<kill-session/>")
236 request.append("<close-session/>")
239 return doWrappedRpc(messageId, request.toString())
242 fun validateRPCXML(rpcRequest: String): Boolean {
244 if (StringUtils.isBlank(rpcRequest)) {
247 val dbf = DocumentBuilderFactory.newInstance()
248 dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
249 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false)
250 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
251 dbf.newDocumentBuilder()
252 .parse(InputSource(StringReader(rpcRequest.replace(RpcMessageUtils.END_PATTERN, ""))))
254 } catch (e: Exception) {
260 fun getMsgId(message: String): String {
261 val matcher = MSG_ID_STRING_PATTERN.matcher(message)
262 if (matcher.find()) {
263 return matcher.group(1)
265 return if (message.contains(RpcMessageUtils.HELLO)) {
270 fun validateChunkedFraming(reply: String): Boolean {
271 val matcher = CHUNKED_FRAMING_PATTERN.matcher(reply)
272 if (!matcher.matches()) {
273 log.debug("Error Reply: {}", reply)
276 val chunkM = CHUNKED_SIZE_PATTERN.matcher(reply)
277 val chunks = ArrayList<MatchResult>()
278 var chunkdataStr = ""
279 while (chunkM.find()) {
280 chunks.add(chunkM.toMatchResult())
281 // extract chunk-data (and later) in bytes
282 val bytes = Integer.parseInt(chunkM.group(1))
283 val chunkdata = reply.substring(chunkM.end()).toByteArray(StandardCharsets.UTF_8)
284 if (bytes > chunkdata.size) {
285 log.debug("Error Reply - wrong chunk size {}", reply)
288 // convert (only) chunk-data part into String
289 chunkdataStr = String(chunkdata, 0, bytes, StandardCharsets.UTF_8)
290 // skip chunk-data part from next match
291 chunkM.region(chunkM.end() + chunkdataStr.length, reply.length)
293 if (!CHUNKED_END_REGEX_PATTERN.equals(reply.substring(chunks[chunks.size - 1].end() + chunkdataStr.length))) {
294 log.debug("Error Reply: {}", reply)
300 fun createHelloString(capabilities: List<String>): String {
301 val helloMessage = StringBuilder()
302 helloMessage.append(RpcMessageUtils.XML_HEADER).append(NEW_LINE)
303 helloMessage.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">").append(NEW_LINE)
304 helloMessage.append(" <capabilities>").append(NEW_LINE)
305 if (capabilities.isNotEmpty()) {
306 capabilities.forEach { cap ->
307 helloMessage.append(" <capability>").append(cap).append("</capability>").append(NEW_LINE)
310 helloMessage.append(" </capabilities>").append(NEW_LINE)
311 helloMessage.append("</hello>").append(NEW_LINE)
312 helloMessage.append(RpcMessageUtils.END_PATTERN)
313 return helloMessage.toString()
316 fun formatRPCRequest(request: String, messageId: String, deviceCapabilities: Set<String>): String {
317 var request = request
318 request = NetconfMessageUtils.formatNetconfMessage(deviceCapabilities, request)
319 request = NetconfMessageUtils.formatXmlHeader(request)
320 request = NetconfMessageUtils.formatRequestMessageId(request, messageId)
326 * Validate and format netconf message. - NC1.0 if no EOM sequence present on `message`,
327 * append. - NC1.1 chunk-encode given message unless it already is chunk encoded
329 * @param deviceCapabilities Set containing Device Capabilities
330 * @param message to format
331 * @return formated message
333 fun formatNetconfMessage(deviceCapabilities: Set<String>, message: String): String {
334 var message = message
335 if (deviceCapabilities.contains(RpcMessageUtils.NETCONF_11_CAPABILITY)) {
336 message = formatChunkedMessage(message)
337 } else if (!message.endsWith(RpcMessageUtils.END_PATTERN)) {
338 message = message + NEW_LINE + RpcMessageUtils.END_PATTERN
344 * Validate and format message according to chunked framing mechanism.
346 * @param message to format
347 * @return formated message
349 fun formatChunkedMessage(message: String): String {
350 var message = message
351 if (message.endsWith(RpcMessageUtils.END_PATTERN)) {
352 // message given had Netconf 1.0 EOM pattern -> remove
353 message = message.substring(0, message.length - RpcMessageUtils.END_PATTERN.length)
355 if (!message.startsWith(RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH)) {
356 // chunk encode message
358 (RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH + message.toByteArray(UTF_8).size + RpcMessageUtils.NEW_LINE + message + RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH
359 + RpcMessageUtils.NEW_LINE)
365 * Ensures xml start directive/declaration appears in the `request`.
367 * @param request RPC request message
368 * @return XML RPC message
370 fun formatXmlHeader(request: String): String {
371 var request = request
372 if (!request.contains(RpcMessageUtils.XML_HEADER)) {
373 if (request.startsWith(RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH)) {
375 request.split("<".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + RpcMessageUtils.XML_HEADER + request.substring(
376 request.split("<".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0].length)
378 request = RpcMessageUtils.XML_HEADER + "\n" + request
384 fun formatRequestMessageId(request: String, messageId: String): String {
385 var request = request
386 if (request.contains(RpcMessageUtils.MESSAGE_ID_STRING)) {
388 request.replaceFirst((RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.NUMBER_BETWEEN_QUOTES_MATCHER).toRegex(),
389 RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.QUOTE + messageId + RpcMessageUtils.QUOTE)
390 } else if (!request.contains(RpcMessageUtils.MESSAGE_ID_STRING) && !request.contains(
391 RpcMessageUtils.HELLO)) {
392 request = request.replaceFirst(RpcMessageUtils.END_OF_RPC_OPEN_TAG.toRegex(),
393 RpcMessageUtils.QUOTE_SPACE + RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.QUOTE + messageId + RpcMessageUtils.QUOTE + ">")
395 return updateRequestLength(request)
398 fun updateRequestLength(request: String): String {
399 if (request.contains(NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH + NEW_LINE)) {
401 Integer.parseInt(request.split(RpcMessageUtils.HASH.toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1].split(
402 NEW_LINE.toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[0])
403 val rpcWithEnding = request.substring(request.indexOf('<'))
405 request.split(RpcMessageUtils.MSGLEN_REGEX_PATTERN.toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1].split(
406 (NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH + NEW_LINE).toRegex()).dropLastWhile(
407 { it.isEmpty() }).toTypedArray()[0]
409 newLen = firstBlock.toByteArray(UTF_8).size
410 if (oldLen != newLen) {
411 return NEW_LINE + RpcMessageUtils.HASH + newLen + NEW_LINE + rpcWithEnding
417 fun checkReply(reply: String?): Boolean {
418 return if (reply != null) {
419 !reply.contains("rpc-error>") || reply.contains("ok/>")