2 * Copyright (c) 2013 Cisco Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.netconf.client;
10 import com.google.common.annotations.VisibleForTesting;
11 import com.google.common.base.Strings;
12 import com.google.common.collect.ImmutableSet;
13 import com.google.common.collect.Interner;
14 import com.google.common.collect.Interners;
15 import io.netty.channel.Channel;
16 import io.netty.channel.ChannelHandlerContext;
17 import io.netty.channel.ChannelInboundHandlerAdapter;
18 import io.netty.util.concurrent.Promise;
20 import javax.xml.xpath.XPathConstants;
21 import javax.xml.xpath.XPathExpression;
22 import javax.xml.xpath.XPathExpressionException;
23 import org.checkerframework.checker.index.qual.NonNegative;
24 import org.eclipse.jdt.annotation.NonNull;
25 import org.opendaylight.netconf.api.NetconfDocumentedException;
26 import org.opendaylight.netconf.api.messages.HelloMessage;
27 import org.opendaylight.netconf.api.messages.NetconfMessage;
28 import org.opendaylight.netconf.api.messages.RpcMessage;
29 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
30 import org.opendaylight.netconf.api.xml.XmlUtil;
31 import org.opendaylight.netconf.common.NetconfTimer;
32 import org.opendaylight.netconf.nettyutil.AbstractChannelInitializer;
33 import org.opendaylight.netconf.nettyutil.NetconfSessionNegotiator;
34 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.SessionIdType;
35 import org.opendaylight.yangtools.yang.common.Uint32;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38 import org.w3c.dom.Document;
39 import org.w3c.dom.Node;
40 import org.w3c.dom.NodeList;
42 // Non-final for mocking
43 class NetconfClientSessionNegotiator
44 extends NetconfSessionNegotiator<NetconfClientSession, NetconfClientSessionListener> {
45 private static final Logger LOG = LoggerFactory.getLogger(NetconfClientSessionNegotiator.class);
47 private static final XPathExpression SESSION_ID_X_PATH = XMLNetconfUtil
48 .compileXPath("/netconf:hello/netconf:session-id");
50 private static final XPathExpression SESSION_ID_X_PATH_NO_NAMESPACE = XMLNetconfUtil
51 .compileXPath("/hello/session-id");
53 private static final String EXI_1_0_CAPABILITY_MARKER = "exi:1.0";
55 private static final Interner<Set<String>> INTERNER = Interners.newWeakInterner();
57 private final RpcMessage startExi;
59 NetconfClientSessionNegotiator(final HelloMessage hello, final RpcMessage startExi,
60 final Promise<NetconfClientSession> promise, final Channel channel, final NetconfTimer timer,
61 final NetconfClientSessionListener sessionListener, final long connectionTimeoutMillis,
62 final @NonNegative int maximumIncomingChunkSize) {
63 super(hello, promise, channel, timer, sessionListener, connectionTimeoutMillis, maximumIncomingChunkSize);
64 this.startExi = startExi;
67 @SuppressWarnings("checkstyle:IllegalCatch")
69 protected void handleMessage(final HelloMessage netconfMessage) throws NetconfDocumentedException {
70 if (!ifNegotiatedAlready()) {
71 LOG.debug("Server hello message received, starting negotiation on channel {}", channel);
74 } catch (final Exception e) {
75 LOG.warn("Unexpected negotiation failure on channel {}", channel, e);
80 final NetconfClientSession session = getSessionForHelloMessage(netconfMessage);
81 replaceHelloMessageInboundHandler(session);
83 // If exi should be used, try to initiate exi communication
84 // Call negotiationSuccessFul after exi negotiation is finished successfully or not
85 if (startExi != null && shouldUseExi(netconfMessage)) {
86 LOG.debug("Netconf session {} should use exi.", session);
87 tryToInitiateExi(session, startExi);
89 // Exi is not supported, release session immediately
90 LOG.debug("Netconf session {} isn't capable of using exi.", session);
91 negotiationSuccessful(session);
96 * Initiates exi communication by sending start-exi message and waiting for positive/negative response.
98 * @param startExiMessage Exi message for initilization of exi communication.
100 void tryToInitiateExi(final NetconfClientSession session, final RpcMessage startExiMessage) {
101 channel.pipeline().addAfter(AbstractChannelInitializer.NETCONF_MESSAGE_DECODER,
102 ExiConfirmationInboundHandler.EXI_CONFIRMED_HANDLER,
103 new ExiConfirmationInboundHandler(session, startExiMessage));
105 session.sendMessage(startExiMessage).addListener(channelFuture -> {
106 if (!channelFuture.isSuccess()) {
107 LOG.warn("Failed to send start-exi message {} on session {}", startExiMessage, session,
108 channelFuture.cause());
109 channel.pipeline().remove(ExiConfirmationInboundHandler.EXI_CONFIRMED_HANDLER);
111 LOG.trace("Start-exi message {} sent to socket on session {}", startExiMessage, session);
116 private boolean shouldUseExi(final HelloMessage helloMsg) {
117 return containsExi10Capability(helloMsg.getDocument()) && containsExi10Capability(localHello().getDocument());
120 private static boolean containsExi10Capability(final Document doc) {
121 final NodeList nList = doc.getElementsByTagName(XmlNetconfConstants.CAPABILITY);
122 for (int i = 0; i < nList.getLength(); i++) {
123 if (nList.item(i).getTextContent().contains(EXI_1_0_CAPABILITY_MARKER)) {
130 private static @NonNull SessionIdType extractSessionId(final Document doc) {
131 String textContent = getSessionIdWithXPath(doc, SESSION_ID_X_PATH);
132 if (Strings.isNullOrEmpty(textContent)) {
133 textContent = getSessionIdWithXPath(doc, SESSION_ID_X_PATH_NO_NAMESPACE);
134 if (Strings.isNullOrEmpty(textContent)) {
135 throw new IllegalStateException("Session id not received from server, hello message: " + XmlUtil
139 return new SessionIdType(Uint32.valueOf(textContent));
142 private static String getSessionIdWithXPath(final Document doc, final XPathExpression sessionIdXPath) {
143 final var sessionIdNode = evaluateXPath(sessionIdXPath, doc);
144 return sessionIdNode != null ? sessionIdNode.getTextContent() : null;
148 protected NetconfClientSession getSession(final NetconfClientSessionListener sessionListener, final Channel channel,
149 final HelloMessage message) {
150 final var sessionId = extractSessionId(message.getDocument());
152 // Copy here is important: it disconnects the strings from the document
153 final var capabilities = INTERNER.intern(ImmutableSet.copyOf(
154 NetconfMessageUtil.extractCapabilitiesFromHello(message .getDocument())));
156 return new NetconfClientSession(sessionListener, channel, sessionId, capabilities);
160 static Node evaluateXPath(final XPathExpression expr, final Object rootNode) {
162 return (Node) expr.evaluate(rootNode, XPathConstants.NODE);
163 } catch (final XPathExpressionException e) {
164 throw new IllegalStateException("Error while evaluating xpath expression " + expr, e);
169 * Handler to process response for start-exi message.
171 private final class ExiConfirmationInboundHandler extends ChannelInboundHandlerAdapter {
172 private static final String EXI_CONFIRMED_HANDLER = "exiConfirmedHandler";
174 private final NetconfClientSession session;
175 private final RpcMessage startExiMessage;
177 ExiConfirmationInboundHandler(final NetconfClientSession session,
178 final RpcMessage startExiMessage) {
179 this.session = session;
180 this.startExiMessage = startExiMessage;
183 @SuppressWarnings("checkstyle:IllegalCatch")
185 public void channelRead(final ChannelHandlerContext ctx, final Object msg) {
186 ctx.pipeline().remove(ExiConfirmationInboundHandler.EXI_CONFIRMED_HANDLER);
188 NetconfMessage netconfMessage = (NetconfMessage) msg;
190 // Ok response to start-exi, try to add exi handlers
191 if (NetconfMessageUtil.isOKMessage(netconfMessage)) {
192 LOG.trace("Positive response on start-exi call received on session {}", session);
194 session.startExiCommunication(startExiMessage);
195 } catch (RuntimeException e) {
196 // Unable to add exi, continue without exi
197 LOG.warn("Unable to start exi communication, Communication will continue without exi on session {}",
202 } else if (NetconfMessageUtil.isErrorMessage(netconfMessage)) {
204 "Error response to start-exi message {}, Communication will continue without exi on session {}",
205 netconfMessage, session);
207 // Unexpected response to start-exi, throwing message away, continue without exi
210 Unexpected response to start-exi message, should be ok, was {}. Communication will continue \
211 without EXI and response message will be thrown away on session {}""", netconfMessage, session);
214 negotiationSuccessful(session);