/* * Copyright 2015-present Open Networking Laboratory * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.onosproject.netconf.ctl.impl; import com.google.common.annotations.Beta; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.ImmutableSet; import org.apache.sshd.client.SshClient; import org.apache.sshd.client.channel.ClientChannel; import org.apache.sshd.client.future.ConnectFuture; import org.apache.sshd.client.future.OpenFuture; import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; import org.onosproject.netconf.NetconfDeviceInfo; import org.onosproject.netconf.NetconfDeviceOutputEvent; import org.onosproject.netconf.NetconfDeviceOutputEvent.Type; import org.onosproject.netconf.NetconfDeviceOutputEventListener; import org.onosproject.netconf.NetconfException; import org.onosproject.netconf.NetconfSession; import org.onosproject.netconf.NetconfSessionFactory; import org.onosproject.netconf.TargetConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Implementation of a NETCONF session to talk to a device. */ public class NetconfSessionMinaImpl implements NetconfSession { private static final Logger log = LoggerFactory .getLogger(NetconfSessionMinaImpl.class); private static final String ENDPATTERN = "]]>]]>"; private static final String MESSAGE_ID_STRING = "message-id"; private static final String HELLO = "<hello"; private static final String NEW_LINE = "\n"; private static final String END_OF_RPC_OPEN_TAG = "\">"; private static final String EQUAL = "="; private static final String NUMBER_BETWEEN_QUOTES_MATCHER = "\"+([0-9]+)+\""; private static final String RPC_OPEN = "<rpc "; private static final String RPC_CLOSE = "</rpc>"; private static final String GET_OPEN = "<get>"; private static final String GET_CLOSE = "</get>"; private static final String WITH_DEFAULT_OPEN = "<with-defaults "; private static final String WITH_DEFAULT_CLOSE = "</with-defaults>"; private static final String DEFAULT_OPERATION_OPEN = "<default-operation>"; private static final String DEFAULT_OPERATION_CLOSE = "</default-operation>"; private static final String SUBTREE_FILTER_OPEN = "<filter type=\"subtree\">"; private static final String SUBTREE_FILTER_CLOSE = "</filter>"; private static final String EDIT_CONFIG_OPEN = "<edit-config>"; private static final String EDIT_CONFIG_CLOSE = "</edit-config>"; private static final String TARGET_OPEN = "<target>"; private static final String TARGET_CLOSE = "</target>"; private static final String CONFIG_OPEN = "<config xmlns:nc=\"urn:ietf:params:xml:ns:netconf:base:1.0\">"; private static final String CONFIG_CLOSE = "</config>"; private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; private static final String NETCONF_BASE_NAMESPACE = "xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\""; private static final String NETCONF_WITH_DEFAULTS_NAMESPACE = "xmlns=\"urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults\""; private static final String SUBSCRIPTION_SUBTREE_FILTER_OPEN = "<filter xmlns:base10=\"urn:ietf:params:xml:ns:netconf:base:1.0\" base10:type=\"subtree\">"; private static final String INTERLEAVE_CAPABILITY_STRING = "urn:ietf:params:netconf:capability:interleave:1.0"; private static final String CAPABILITY_REGEX = "<capability>\\s*(.*?)\\s*</capability>"; private static final Pattern CAPABILITY_REGEX_PATTERN = Pattern.compile(CAPABILITY_REGEX); private static final String SESSION_ID_REGEX = "<session-id>\\s*(.*?)\\s*</session-id>"; private static final Pattern SESSION_ID_REGEX_PATTERN = Pattern.compile(SESSION_ID_REGEX); private static final String RSA = "RSA"; private static final String DSA = "DSA"; private String sessionID; private final AtomicInteger messageIdInteger = new AtomicInteger(1); protected final NetconfDeviceInfo deviceInfo; private Iterable<String> onosCapabilities = Collections.singletonList("urn:ietf:params:netconf:base:1.0"); /* NOTE: the "serverHelloResponseOld" is deprecated in 1.10.0 and should eventually be removed */ @Deprecated private String serverHelloResponseOld; private final Set<String> deviceCapabilities = new LinkedHashSet<>(); private NetconfStreamHandler streamHandler; private Map<Integer, CompletableFuture<String>> replies; private List<String> errorReplies; private boolean subscriptionConnected = false; private String notificationFilterSchema = null; private final Collection<NetconfDeviceOutputEventListener> primaryListeners = new CopyOnWriteArrayList<>(); private final Collection<NetconfSession> children = new CopyOnWriteArrayList<>(); private ClientChannel channel = null; private ClientSession session = null; private SshClient client = null; public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo) throws NetconfException { this.deviceInfo = deviceInfo; replies = new ConcurrentHashMap<>(); errorReplies = new ArrayList<>(); startConnection(); } private void startConnection() throws NetconfException { try { startClient(); } catch (IOException e) { throw new NetconfException("Failed to establish SSH with device " + deviceInfo, e); } } private void startClient() throws IOException { client = SshClient.setUpDefaultClient(); client.start(); client.setKeyPairProvider(new SimpleGeneratorHostKeyProvider()); startSession(); } private void startSession() throws IOException { final ConnectFuture connectFuture; connectFuture = client.connect(deviceInfo.name(), deviceInfo.ip().toString(), deviceInfo.port()) .verify(NetconfControllerImpl.netconfConnectTimeout, TimeUnit.SECONDS); session = connectFuture.getSession(); //Using the device ssh key if possible if (deviceInfo.getKey() != null) { ByteBuffer buf = StandardCharsets.UTF_8.encode(CharBuffer.wrap(deviceInfo.getKey())); byte[] byteKey = new byte[buf.limit()]; buf.get(byteKey); PublicKey key; try { key = getPublicKey(byteKey, RSA); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { try { key = getPublicKey(byteKey, DSA); } catch (NoSuchAlgorithmException | InvalidKeySpecException e1) { throw new NetconfException("Failed to authenticate session with device " + deviceInfo + "check key to be the " + "proper DSA or RSA key", e1); } } //privateKye can set tu null because is not used by the method. session.addPublicKeyIdentity(new KeyPair(key, null)); } else { session.addPasswordIdentity(deviceInfo.password()); } session.auth().verify(NetconfControllerImpl.netconfConnectTimeout, TimeUnit.SECONDS); Set<ClientSession.ClientSessionEvent> event = session.waitFor( ImmutableSet.of(ClientSession.ClientSessionEvent.WAIT_AUTH, ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.AUTHED), 0); if (!event.contains(ClientSession.ClientSessionEvent.AUTHED)) { log.debug("Session closed {} {}", event, session.isClosed()); throw new NetconfException("Failed to authenticate session with device " + deviceInfo + "check the user/pwd or key"); } openChannel(); } private PublicKey getPublicKey(byte[] keyBytes, String type) throws NoSuchAlgorithmException, InvalidKeySpecException { X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); KeyFactory kf = KeyFactory.getInstance(type); return kf.generatePublic(spec); } private void openChannel() throws IOException { channel = session.createSubsystemChannel("netconf"); OpenFuture channelFuture = channel.open(); if (channelFuture.await(NetconfControllerImpl.netconfConnectTimeout, TimeUnit.SECONDS)) { if (channelFuture.isOpened()) { streamHandler = new NetconfStreamThread(channel.getInvertedOut(), channel.getInvertedIn(), channel.getInvertedErr(), deviceInfo, new NetconfSessionDelegateImpl(), replies); } else { throw new NetconfException("Failed to open channel with device " + deviceInfo); } sendHello(); } } @Beta protected void startSubscriptionStream(String filterSchema) throws NetconfException { boolean openNewSession = false; if (!deviceCapabilities.contains(INTERLEAVE_CAPABILITY_STRING)) { log.info("Device {} doesn't support interleave, creating child session", deviceInfo); openNewSession = true; } else if (subscriptionConnected && notificationFilterSchema != null && !Objects.equal(filterSchema, notificationFilterSchema)) { // interleave supported and existing filter is NOT "no filtering" // and was requested with different filtering schema log.info("Cannot use existing session for subscription {} ({})", deviceInfo, filterSchema); openNewSession = true; } if (openNewSession) { log.info("Creating notification session to {} with filter {}", deviceInfo, filterSchema); NetconfSession child = new NotificationSession(deviceInfo); child.addDeviceOutputListener(new NotificationForwarder()); child.startSubscription(filterSchema); children.add(child); return; } // request to start interleaved notification session String reply = sendRequest(createSubscriptionString(filterSchema)); if (!checkReply(reply)) { throw new NetconfException("Subscription not successful with device " + deviceInfo + " with reply " + reply); } subscriptionConnected = true; } @Override public void startSubscription() throws NetconfException { if (!subscriptionConnected) { startSubscriptionStream(null); } streamHandler.setEnableNotifications(true); } @Beta @Override public void startSubscription(String filterSchema) throws NetconfException { if (!subscriptionConnected) { notificationFilterSchema = filterSchema; startSubscriptionStream(filterSchema); } streamHandler.setEnableNotifications(true); } @Beta protected String createSubscriptionString(String filterSchema) { StringBuilder subscriptionbuffer = new StringBuilder(); subscriptionbuffer.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"); subscriptionbuffer.append(" <create-subscription\n"); subscriptionbuffer.append("xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">\n"); // FIXME Only subtree filtering supported at the moment. if (filterSchema != null) { subscriptionbuffer.append(" "); subscriptionbuffer.append(SUBSCRIPTION_SUBTREE_FILTER_OPEN).append(NEW_LINE); subscriptionbuffer.append(filterSchema).append(NEW_LINE); subscriptionbuffer.append(" "); subscriptionbuffer.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE); } subscriptionbuffer.append(" </create-subscription>\n"); subscriptionbuffer.append("</rpc>\n"); subscriptionbuffer.append(ENDPATTERN); return subscriptionbuffer.toString(); } @Override public void endSubscription() throws NetconfException { if (subscriptionConnected) { streamHandler.setEnableNotifications(false); } else { throw new NetconfException("Subscription does not exist."); } } private void sendHello() throws NetconfException { serverHelloResponseOld = sendRequest(createHelloString(), true); Matcher capabilityMatcher = CAPABILITY_REGEX_PATTERN.matcher(serverHelloResponseOld); while (capabilityMatcher.find()) { deviceCapabilities.add(capabilityMatcher.group(1)); } sessionID = String.valueOf(-1); Matcher sessionIDMatcher = SESSION_ID_REGEX_PATTERN.matcher(serverHelloResponseOld); if (sessionIDMatcher.find()) { sessionID = sessionIDMatcher.group(1); } else { throw new NetconfException("Missing SessionID in server hello " + "reponse."); } } private String createHelloString() { StringBuilder hellobuffer = new StringBuilder(); hellobuffer.append(XML_HEADER); hellobuffer.append("\n"); hellobuffer.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"); hellobuffer.append(" <capabilities>\n"); onosCapabilities.forEach( cap -> hellobuffer.append(" <capability>") .append(cap) .append("</capability>\n")); hellobuffer.append(" </capabilities>\n"); hellobuffer.append("</hello>\n"); hellobuffer.append(ENDPATTERN); return hellobuffer.toString(); } @Override public void checkAndReestablish() throws NetconfException { try { if (client.isClosed()) { log.debug("Trying to restart the whole SSH connection with {}", deviceInfo.getDeviceId()); cleanUp(); startConnection(); } else if (session.isClosed()) { log.debug("Trying to restart the session with {}", session, deviceInfo.getDeviceId()); cleanUp(); startSession(); } else if (channel.isClosed()) { log.debug("Trying to reopen the channel with {}", deviceInfo.getDeviceId()); cleanUp(); openChannel(); } if (subscriptionConnected) { log.debug("Restarting subscription with {}", deviceInfo.getDeviceId()); subscriptionConnected = false; startSubscription(notificationFilterSchema); } } catch (IOException e) { log.error("Can't reopen connection for device {}", e.getMessage()); throw new NetconfException("Cannot re-open the connection with device" + deviceInfo, e); } } private void cleanUp() { //makes sure everything is at a clean state. replies.clear(); } @Override public String requestSync(String request) throws NetconfException { if (!request.contains(ENDPATTERN)) { request = request + NEW_LINE + ENDPATTERN; } String reply = sendRequest(request); checkReply(reply); return reply; } @Override @Deprecated public CompletableFuture<String> request(String request) { return streamHandler.sendMessage(request); } private CompletableFuture<String> request(String request, int messageId) { return streamHandler.sendMessage(request, messageId); } private String sendRequest(String request) throws NetconfException { return sendRequest(request, false); } private String sendRequest(String request, boolean isHello) throws NetconfException { checkAndReestablish(); int messageId = -1; if (!isHello) { messageId = messageIdInteger.getAndIncrement(); } request = formatRequestMessageId(request, messageId); request = formatXmlHeader(request); CompletableFuture<String> futureReply = request(request, messageId); int replyTimeout = NetconfControllerImpl.netconfReplyTimeout; String rp; try { rp = futureReply.get(replyTimeout, TimeUnit.SECONDS); replies.remove(messageId); } catch (InterruptedException | ExecutionException | TimeoutException e) { throw new NetconfException("No matching reply for request " + request, e); } log.debug("Result {} from request {} to device {}", rp, request, deviceInfo); return rp.trim(); } private String formatRequestMessageId(String request, int messageId) { if (request.contains(MESSAGE_ID_STRING)) { //FIXME if application provides his own counting of messages this fails that count request = request.replaceFirst(MESSAGE_ID_STRING + EQUAL + NUMBER_BETWEEN_QUOTES_MATCHER, MESSAGE_ID_STRING + EQUAL + "\"" + messageId + "\""); } else if (!request.contains(MESSAGE_ID_STRING) && !request.contains(HELLO)) { //FIXME find out a better way to enforce the presence of message-id request = request.replaceFirst(END_OF_RPC_OPEN_TAG, "\" " + MESSAGE_ID_STRING + EQUAL + "\"" + messageId + "\"" + ">"); } return request; } private String formatXmlHeader(String request) { if (!request.contains(XML_HEADER)) { //FIXME if application provieds his own XML header of different type there is a clash request = XML_HEADER + "\n" + request; } return request; } @Override public String doWrappedRpc(String request) throws NetconfException { StringBuilder rpc = new StringBuilder(XML_HEADER); rpc.append(RPC_OPEN); rpc.append(MESSAGE_ID_STRING); rpc.append(EQUAL); rpc.append("\""); rpc.append(messageIdInteger.get()); rpc.append("\" "); rpc.append(NETCONF_BASE_NAMESPACE).append(">\n"); rpc.append(request); rpc.append(RPC_CLOSE).append(NEW_LINE); rpc.append(ENDPATTERN); String reply = sendRequest(rpc.toString()); checkReply(reply); return reply; } @Override public String get(String request) throws NetconfException { return requestSync(request); } @Override public String get(String filterSchema, String withDefaultsMode) throws NetconfException { StringBuilder rpc = new StringBuilder(XML_HEADER); rpc.append(RPC_OPEN); rpc.append(MESSAGE_ID_STRING); rpc.append(EQUAL); rpc.append("\""); rpc.append(messageIdInteger.get()); rpc.append("\" "); rpc.append(NETCONF_BASE_NAMESPACE).append(">\n"); rpc.append(GET_OPEN).append(NEW_LINE); if (filterSchema != null) { rpc.append(SUBTREE_FILTER_OPEN).append(NEW_LINE); rpc.append(filterSchema).append(NEW_LINE); rpc.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE); } if (withDefaultsMode != null) { rpc.append(WITH_DEFAULT_OPEN).append(NETCONF_WITH_DEFAULTS_NAMESPACE).append(">"); rpc.append(withDefaultsMode).append(WITH_DEFAULT_CLOSE).append(NEW_LINE); } rpc.append(GET_CLOSE).append(NEW_LINE); rpc.append(RPC_CLOSE).append(NEW_LINE); rpc.append(ENDPATTERN); String reply = sendRequest(rpc.toString()); checkReply(reply); return reply; } @Override public String getConfig(TargetConfig netconfTargetConfig) throws NetconfException { return getConfig(netconfTargetConfig, null); } @Override public String getConfig(String netconfTargetConfig) throws NetconfException { return getConfig(TargetConfig.toTargetConfig(netconfTargetConfig)); } @Override public String getConfig(String netconfTargetConfig, String configurationFilterSchema) throws NetconfException { return getConfig(TargetConfig.toTargetConfig(netconfTargetConfig), configurationFilterSchema); } @Override public String getConfig(TargetConfig netconfTargetConfig, String configurationSchema) throws NetconfException { StringBuilder rpc = new StringBuilder(XML_HEADER); rpc.append("<rpc "); rpc.append(MESSAGE_ID_STRING); rpc.append(EQUAL); rpc.append("\""); rpc.append(messageIdInteger.get()); rpc.append("\" "); rpc.append("xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"); rpc.append("<get-config>\n"); rpc.append("<source>\n"); rpc.append("<").append(netconfTargetConfig).append("/>"); rpc.append("</source>"); if (configurationSchema != null) { rpc.append("<filter type=\"subtree\">\n"); rpc.append(configurationSchema).append("\n"); rpc.append("</filter>\n"); } rpc.append("</get-config>\n"); rpc.append("</rpc>\n"); rpc.append(ENDPATTERN); String reply = sendRequest(rpc.toString()); return checkReply(reply) ? reply : "ERROR " + reply; } @Override public boolean editConfig(String newConfiguration) throws NetconfException { newConfiguration = newConfiguration + ENDPATTERN; return checkReply(sendRequest(newConfiguration)); } @Override public boolean editConfig(String netconfTargetConfig, String mode, String newConfiguration) throws NetconfException { return editConfig(TargetConfig.toTargetConfig(netconfTargetConfig), mode, newConfiguration); } @Override public boolean editConfig(TargetConfig netconfTargetConfig, String mode, String newConfiguration) throws NetconfException { newConfiguration = newConfiguration.trim(); StringBuilder rpc = new StringBuilder(XML_HEADER); rpc.append(RPC_OPEN); rpc.append(MESSAGE_ID_STRING); rpc.append(EQUAL); rpc.append("\""); rpc.append(messageIdInteger.get()); rpc.append("\" "); rpc.append(NETCONF_BASE_NAMESPACE).append(">\n"); rpc.append(EDIT_CONFIG_OPEN).append("\n"); rpc.append(TARGET_OPEN); rpc.append("<").append(netconfTargetConfig).append("/>"); rpc.append(TARGET_CLOSE).append("\n"); if (mode != null) { rpc.append(DEFAULT_OPERATION_OPEN); rpc.append(mode); rpc.append(DEFAULT_OPERATION_CLOSE).append("\n"); } rpc.append(CONFIG_OPEN).append("\n"); rpc.append(newConfiguration); rpc.append(CONFIG_CLOSE).append("\n"); rpc.append(EDIT_CONFIG_CLOSE).append("\n"); rpc.append(RPC_CLOSE); rpc.append(ENDPATTERN); log.debug(rpc.toString()); String reply = sendRequest(rpc.toString()); return checkReply(reply); } @Override public boolean copyConfig(String netconfTargetConfig, String newConfiguration) throws NetconfException { return copyConfig(TargetConfig.toTargetConfig(netconfTargetConfig), newConfiguration); } @Override public boolean copyConfig(TargetConfig netconfTargetConfig, String newConfiguration) throws NetconfException { newConfiguration = newConfiguration.trim(); if (!newConfiguration.startsWith("<config>")) { newConfiguration = "<config>" + newConfiguration + "</config>"; } StringBuilder rpc = new StringBuilder(XML_HEADER); rpc.append(RPC_OPEN); rpc.append(NETCONF_BASE_NAMESPACE).append(">\n"); rpc.append("<copy-config>"); rpc.append("<target>"); rpc.append("<").append(netconfTargetConfig).append("/>"); rpc.append("</target>"); rpc.append("<source>"); rpc.append(newConfiguration); rpc.append("</source>"); rpc.append("</copy-config>"); rpc.append("</rpc>"); rpc.append(ENDPATTERN); return checkReply(sendRequest(rpc.toString())); } @Override public boolean deleteConfig(String netconfTargetConfig) throws NetconfException { return deleteConfig(TargetConfig.toTargetConfig(netconfTargetConfig)); } @Override public boolean deleteConfig(TargetConfig netconfTargetConfig) throws NetconfException { if (netconfTargetConfig.equals(TargetConfig.RUNNING)) { log.warn("Target configuration for delete operation can't be \"running\"", netconfTargetConfig); return false; } StringBuilder rpc = new StringBuilder(XML_HEADER); rpc.append("<rpc>"); rpc.append("<delete-config>"); rpc.append("<target>"); rpc.append("<").append(netconfTargetConfig).append("/>"); rpc.append("</target>"); rpc.append("</delete-config>"); rpc.append("</rpc>"); rpc.append(ENDPATTERN); return checkReply(sendRequest(rpc.toString())); } @Override public boolean lock(String configType) throws NetconfException { StringBuilder rpc = new StringBuilder(XML_HEADER); rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"); rpc.append("<lock>"); rpc.append("<target>"); rpc.append("<"); rpc.append(configType); rpc.append("/>"); rpc.append("</target>"); rpc.append("</lock>"); rpc.append("</rpc>"); rpc.append(ENDPATTERN); String lockReply = sendRequest(rpc.toString()); return checkReply(lockReply); } @Override public boolean unlock(String configType) throws NetconfException { StringBuilder rpc = new StringBuilder(XML_HEADER); rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"); rpc.append("<unlock>"); rpc.append("<target>"); rpc.append("<"); rpc.append(configType); rpc.append("/>"); rpc.append("</target>"); rpc.append("</unlock>"); rpc.append("</rpc>"); rpc.append(ENDPATTERN); String unlockReply = sendRequest(rpc.toString()); return checkReply(unlockReply); } @Override public boolean lock() throws NetconfException { return lock("running"); } @Override public boolean unlock() throws NetconfException { return unlock("running"); } @Override public boolean close() throws NetconfException { return close(false); } private boolean close(boolean force) throws NetconfException { StringBuilder rpc = new StringBuilder(); rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">"); if (force) { rpc.append("<kill-session/>"); } else { rpc.append("<close-session/>"); } rpc.append("</rpc>"); rpc.append(ENDPATTERN); return checkReply(sendRequest(rpc.toString())) || close(true); } @Override public String getSessionId() { return sessionID; } @Override public Set<String> getDeviceCapabilitiesSet() { return Collections.unmodifiableSet(deviceCapabilities); } @Deprecated @Override public String getServerCapabilities() { return serverHelloResponseOld; } @Deprecated @Override public void setDeviceCapabilities(List<String> capabilities) { onosCapabilities = capabilities; } @Override public void setOnosCapabilities(Iterable<String> capabilities) { onosCapabilities = capabilities; } @Override public void addDeviceOutputListener(NetconfDeviceOutputEventListener listener) { streamHandler.addDeviceEventListener(listener); primaryListeners.add(listener); } @Override public void removeDeviceOutputListener(NetconfDeviceOutputEventListener listener) { primaryListeners.remove(listener); streamHandler.removeDeviceEventListener(listener); } private boolean checkReply(String reply) throws NetconfException { if (reply != null) { if (!reply.contains("<rpc-error>")) { log.debug("Device {} sent reply {}", deviceInfo, reply); return true; } else if (reply.contains("<ok/>") || (reply.contains("<rpc-error>") && reply.contains("warning"))) { log.debug("Device {} sent reply {}", deviceInfo, reply); return true; } } log.warn("Device {} has error in reply {}", deviceInfo, reply); return false; } static class NotificationSession extends NetconfSessionMinaImpl { private String notificationFilter; NotificationSession(NetconfDeviceInfo deviceInfo) throws NetconfException { super(deviceInfo); } @Override protected void startSubscriptionStream(String filterSchema) throws NetconfException { notificationFilter = filterSchema; requestSync(createSubscriptionString(filterSchema)); } @Override public String toString() { return MoreObjects.toStringHelper(getClass()) .add("deviceInfo", deviceInfo) .add("sessionID", getSessionId()) .add("notificationFilter", notificationFilter) .toString(); } } /** * Listener attached to child session for notification streaming. * <p> * Forwards all notification event from child session to primary session * listeners. */ private final class NotificationForwarder implements NetconfDeviceOutputEventListener { @Override public boolean isRelevant(NetconfDeviceOutputEvent event) { return event.type() == Type.DEVICE_NOTIFICATION; } @Override public void event(NetconfDeviceOutputEvent event) { primaryListeners.forEach(lsnr -> { if (lsnr.isRelevant(event)) { lsnr.event(event); } }); } } public class NetconfSessionDelegateImpl implements NetconfSessionDelegate { @Override public void notify(NetconfDeviceOutputEvent event) { Optional<Integer> messageId = event.getMessageID(); log.debug("messageID {}, waiting replies messageIDs {}", messageId, replies.keySet()); if (!messageId.isPresent()) { errorReplies.add(event.getMessagePayload()); log.error("Device {} sent error reply {}", event.getDeviceInfo(), event.getMessagePayload()); return; } CompletableFuture<String> completedReply = replies.get(messageId.get()); if (completedReply != null) { completedReply.complete(event.getMessagePayload()); } } } public static class MinaSshNetconfSessionFactory implements NetconfSessionFactory { @Override public NetconfSession createNetconfSession(NetconfDeviceInfo netconfDeviceInfo) throws NetconfException { return new NetconfSessionMinaImpl(netconfDeviceInfo); } } }