blob: 0e2e3ea306e34662b6afe0a9a4202b7aa68e9459 [file] [log] [blame]
Andrea Campanella7bbe7b12017-05-03 16:03:38 -07001/*
Brian O'Connora09fe5b2017-08-03 21:12:30 -07002 * Copyright 2015-present Open Networking Foundation
Andrea Campanella7bbe7b12017-05-03 16:03:38 -07003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.onosproject.netconf.ctl.impl;
18
19import com.google.common.annotations.Beta;
20import com.google.common.base.MoreObjects;
21import com.google.common.base.Objects;
Kamil Stasiak9f59f442017-05-02 11:02:24 +020022import com.google.common.collect.ImmutableList;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070023import com.google.common.collect.ImmutableSet;
24import org.apache.sshd.client.SshClient;
25import org.apache.sshd.client.channel.ClientChannel;
26import org.apache.sshd.client.future.ConnectFuture;
27import org.apache.sshd.client.future.OpenFuture;
28import org.apache.sshd.client.session.ClientSession;
Sean Condon7347de92017-07-21 12:17:25 +010029import org.apache.sshd.common.FactoryManager;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070030import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -070031import org.onosproject.netconf.DatastoreId;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070032import org.onosproject.netconf.NetconfDeviceInfo;
33import org.onosproject.netconf.NetconfDeviceOutputEvent;
34import org.onosproject.netconf.NetconfDeviceOutputEvent.Type;
35import org.onosproject.netconf.NetconfDeviceOutputEventListener;
36import org.onosproject.netconf.NetconfException;
37import org.onosproject.netconf.NetconfSession;
38import org.onosproject.netconf.NetconfSessionFactory;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070039import org.slf4j.Logger;
40import org.slf4j.LoggerFactory;
41import java.io.IOException;
Kamil Stasiak9f59f442017-05-02 11:02:24 +020042import java.io.UnsupportedEncodingException;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070043import java.nio.ByteBuffer;
44import java.nio.CharBuffer;
45import java.nio.charset.StandardCharsets;
46import java.security.KeyFactory;
47import java.security.KeyPair;
48import java.security.NoSuchAlgorithmException;
49import java.security.PublicKey;
50import java.security.spec.InvalidKeySpecException;
51import java.security.spec.X509EncodedKeySpec;
Kamil Stasiak9f59f442017-05-02 11:02:24 +020052import java.util.LinkedHashSet;
53import java.util.Map;
54import java.util.Set;
55import java.util.List;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070056import java.util.Collection;
57import java.util.Collections;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070058import java.util.Optional;
Kamil Stasiak9f59f442017-05-02 11:02:24 +020059import java.util.ArrayList;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070060import java.util.concurrent.CompletableFuture;
61import java.util.concurrent.ConcurrentHashMap;
62import java.util.concurrent.CopyOnWriteArrayList;
63import java.util.concurrent.ExecutionException;
64import java.util.concurrent.TimeUnit;
65import java.util.concurrent.TimeoutException;
66import java.util.concurrent.atomic.AtomicInteger;
67import java.util.regex.Matcher;
68import java.util.regex.Pattern;
69
70/**
71 * Implementation of a NETCONF session to talk to a device.
72 */
73public class NetconfSessionMinaImpl implements NetconfSession {
74
75 private static final Logger log = LoggerFactory
76 .getLogger(NetconfSessionMinaImpl.class);
77
78 private static final String ENDPATTERN = "]]>]]>";
79 private static final String MESSAGE_ID_STRING = "message-id";
80 private static final String HELLO = "<hello";
81 private static final String NEW_LINE = "\n";
82 private static final String END_OF_RPC_OPEN_TAG = "\">";
83 private static final String EQUAL = "=";
84 private static final String NUMBER_BETWEEN_QUOTES_MATCHER = "\"+([0-9]+)+\"";
85 private static final String RPC_OPEN = "<rpc ";
86 private static final String RPC_CLOSE = "</rpc>";
87 private static final String GET_OPEN = "<get>";
88 private static final String GET_CLOSE = "</get>";
89 private static final String WITH_DEFAULT_OPEN = "<with-defaults ";
90 private static final String WITH_DEFAULT_CLOSE = "</with-defaults>";
91 private static final String DEFAULT_OPERATION_OPEN = "<default-operation>";
92 private static final String DEFAULT_OPERATION_CLOSE = "</default-operation>";
93 private static final String SUBTREE_FILTER_OPEN = "<filter type=\"subtree\">";
94 private static final String SUBTREE_FILTER_CLOSE = "</filter>";
95 private static final String EDIT_CONFIG_OPEN = "<edit-config>";
96 private static final String EDIT_CONFIG_CLOSE = "</edit-config>";
97 private static final String TARGET_OPEN = "<target>";
98 private static final String TARGET_CLOSE = "</target>";
99 private static final String CONFIG_OPEN = "<config xmlns:nc=\"urn:ietf:params:xml:ns:netconf:base:1.0\">";
100 private static final String CONFIG_CLOSE = "</config>";
101 private static final String XML_HEADER =
102 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
103 private static final String NETCONF_BASE_NAMESPACE =
104 "xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\"";
105 private static final String NETCONF_WITH_DEFAULTS_NAMESPACE =
106 "xmlns=\"urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults\"";
107 private static final String SUBSCRIPTION_SUBTREE_FILTER_OPEN =
108 "<filter xmlns:base10=\"urn:ietf:params:xml:ns:netconf:base:1.0\" base10:type=\"subtree\">";
109
110 private static final String INTERLEAVE_CAPABILITY_STRING = "urn:ietf:params:netconf:capability:interleave:1.0";
111
112 private static final String CAPABILITY_REGEX = "<capability>\\s*(.*?)\\s*</capability>";
113 private static final Pattern CAPABILITY_REGEX_PATTERN = Pattern.compile(CAPABILITY_REGEX);
114
115 private static final String SESSION_ID_REGEX = "<session-id>\\s*(.*?)\\s*</session-id>";
116 private static final Pattern SESSION_ID_REGEX_PATTERN = Pattern.compile(SESSION_ID_REGEX);
117 private static final String RSA = "RSA";
118 private static final String DSA = "DSA";
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200119 private static final String HASH = "#";
120 private static final String LF = "\n";
121 private static final String MSGLEN_REGEX_PATTERN = "\n#\\d+\n";
122 private static final String NETCONF_10_CAPABILITY = "urn:ietf:params:netconf:base:1.0";
123 private static final String NETCONF_11_CAPABILITY = "urn:ietf:params:netconf:base:1.1";
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700124
125 private String sessionID;
126 private final AtomicInteger messageIdInteger = new AtomicInteger(1);
127 protected final NetconfDeviceInfo deviceInfo;
128 private Iterable<String> onosCapabilities =
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200129 ImmutableList.of(NETCONF_10_CAPABILITY, NETCONF_11_CAPABILITY);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700130
131 /* NOTE: the "serverHelloResponseOld" is deprecated in 1.10.0 and should eventually be removed */
132 @Deprecated
133 private String serverHelloResponseOld;
134 private final Set<String> deviceCapabilities = new LinkedHashSet<>();
135 private NetconfStreamHandler streamHandler;
136 private Map<Integer, CompletableFuture<String>> replies;
Sean Condon7347de92017-07-21 12:17:25 +0100137 private List<String> errorReplies; // Not sure why we need this?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700138 private boolean subscriptionConnected = false;
139 private String notificationFilterSchema = null;
140
141 private final Collection<NetconfDeviceOutputEventListener> primaryListeners =
142 new CopyOnWriteArrayList<>();
143 private final Collection<NetconfSession> children =
144 new CopyOnWriteArrayList<>();
145
146
147 private ClientChannel channel = null;
148 private ClientSession session = null;
149 private SshClient client = null;
150
151
152 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo) throws NetconfException {
153 this.deviceInfo = deviceInfo;
154 replies = new ConcurrentHashMap<>();
155 errorReplies = new ArrayList<>();
156 startConnection();
157 }
158
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200159 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo, List<String> capabilities) throws NetconfException {
160 this.deviceInfo = deviceInfo;
161 replies = new ConcurrentHashMap<>();
162 errorReplies = new ArrayList<>();
163 setOnosCapabilities(capabilities);
164 startConnection();
165 }
166
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700167 private void startConnection() throws NetconfException {
168 try {
169 startClient();
170 } catch (IOException e) {
171 throw new NetconfException("Failed to establish SSH with device " + deviceInfo, e);
172 }
173 }
174
175 private void startClient() throws IOException {
176 client = SshClient.setUpDefaultClient();
Sean Condon7347de92017-07-21 12:17:25 +0100177 int replyTimeoutSec = NetconfControllerImpl.netconfIdleTimeout;
178 client.getProperties().putIfAbsent(FactoryManager.IDLE_TIMEOUT,
179 TimeUnit.SECONDS.toMillis(replyTimeoutSec));
180 client.getProperties().putIfAbsent(FactoryManager.NIO2_READ_TIMEOUT,
181 TimeUnit.SECONDS.toMillis(replyTimeoutSec + 15L));
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700182 client.start();
183 client.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
184 startSession();
185 }
186
187 private void startSession() throws IOException {
188 final ConnectFuture connectFuture;
189 connectFuture = client.connect(deviceInfo.name(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200190 deviceInfo.ip().toString(),
191 deviceInfo.port())
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700192 .verify(NetconfControllerImpl.netconfConnectTimeout, TimeUnit.SECONDS);
193 session = connectFuture.getSession();
194 //Using the device ssh key if possible
195 if (deviceInfo.getKey() != null) {
196 ByteBuffer buf = StandardCharsets.UTF_8.encode(CharBuffer.wrap(deviceInfo.getKey()));
197 byte[] byteKey = new byte[buf.limit()];
198 buf.get(byteKey);
199 PublicKey key;
200 try {
201 key = getPublicKey(byteKey, RSA);
202 } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
203 try {
204 key = getPublicKey(byteKey, DSA);
205 } catch (NoSuchAlgorithmException | InvalidKeySpecException e1) {
206 throw new NetconfException("Failed to authenticate session with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200207 deviceInfo + "check key to be the " +
208 "proper DSA or RSA key", e1);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700209 }
210 }
211 //privateKye can set tu null because is not used by the method.
212 session.addPublicKeyIdentity(new KeyPair(key, null));
213 } else {
214 session.addPasswordIdentity(deviceInfo.password());
215 }
216 session.auth().verify(NetconfControllerImpl.netconfConnectTimeout, TimeUnit.SECONDS);
217 Set<ClientSession.ClientSessionEvent> event = session.waitFor(
218 ImmutableSet.of(ClientSession.ClientSessionEvent.WAIT_AUTH,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200219 ClientSession.ClientSessionEvent.CLOSED,
220 ClientSession.ClientSessionEvent.AUTHED), 0);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700221
222 if (!event.contains(ClientSession.ClientSessionEvent.AUTHED)) {
223 log.debug("Session closed {} {}", event, session.isClosed());
224 throw new NetconfException("Failed to authenticate session with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200225 deviceInfo + "check the user/pwd or key");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700226 }
227 openChannel();
228 }
229
230 private PublicKey getPublicKey(byte[] keyBytes, String type)
231 throws NoSuchAlgorithmException, InvalidKeySpecException {
232
233 X509EncodedKeySpec spec =
234 new X509EncodedKeySpec(keyBytes);
235 KeyFactory kf = KeyFactory.getInstance(type);
236 return kf.generatePublic(spec);
237 }
238
239 private void openChannel() throws IOException {
240 channel = session.createSubsystemChannel("netconf");
241 OpenFuture channelFuture = channel.open();
242 if (channelFuture.await(NetconfControllerImpl.netconfConnectTimeout, TimeUnit.SECONDS)) {
243 if (channelFuture.isOpened()) {
244 streamHandler = new NetconfStreamThread(channel.getInvertedOut(), channel.getInvertedIn(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200245 channel.getInvertedErr(), deviceInfo,
246 new NetconfSessionDelegateImpl(), replies);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700247 } else {
248 throw new NetconfException("Failed to open channel with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200249 deviceInfo);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700250 }
251 sendHello();
252 }
253 }
254
255
256 @Beta
257 protected void startSubscriptionStream(String filterSchema) throws NetconfException {
258 boolean openNewSession = false;
259 if (!deviceCapabilities.contains(INTERLEAVE_CAPABILITY_STRING)) {
260 log.info("Device {} doesn't support interleave, creating child session", deviceInfo);
261 openNewSession = true;
262
263 } else if (subscriptionConnected &&
264 notificationFilterSchema != null &&
265 !Objects.equal(filterSchema, notificationFilterSchema)) {
266 // interleave supported and existing filter is NOT "no filtering"
267 // and was requested with different filtering schema
268 log.info("Cannot use existing session for subscription {} ({})",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200269 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700270 openNewSession = true;
271 }
272
273 if (openNewSession) {
274 log.info("Creating notification session to {} with filter {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200275 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700276 NetconfSession child = new NotificationSession(deviceInfo);
277
278 child.addDeviceOutputListener(new NotificationForwarder());
279
280 child.startSubscription(filterSchema);
281 children.add(child);
282 return;
283 }
284
285 // request to start interleaved notification session
286 String reply = sendRequest(createSubscriptionString(filterSchema));
287 if (!checkReply(reply)) {
288 throw new NetconfException("Subscription not successful with device "
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200289 + deviceInfo + " with reply " + reply);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700290 }
291 subscriptionConnected = true;
292 }
293
294 @Override
295 public void startSubscription() throws NetconfException {
296 if (!subscriptionConnected) {
297 startSubscriptionStream(null);
298 }
299 streamHandler.setEnableNotifications(true);
300 }
301
302 @Beta
303 @Override
304 public void startSubscription(String filterSchema) throws NetconfException {
305 if (!subscriptionConnected) {
306 notificationFilterSchema = filterSchema;
307 startSubscriptionStream(filterSchema);
308 }
309 streamHandler.setEnableNotifications(true);
310 }
311
312 @Beta
313 protected String createSubscriptionString(String filterSchema) {
314 StringBuilder subscriptionbuffer = new StringBuilder();
315 subscriptionbuffer.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
316 subscriptionbuffer.append(" <create-subscription\n");
317 subscriptionbuffer.append("xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">\n");
318 // FIXME Only subtree filtering supported at the moment.
319 if (filterSchema != null) {
320 subscriptionbuffer.append(" ");
321 subscriptionbuffer.append(SUBSCRIPTION_SUBTREE_FILTER_OPEN).append(NEW_LINE);
322 subscriptionbuffer.append(filterSchema).append(NEW_LINE);
323 subscriptionbuffer.append(" ");
324 subscriptionbuffer.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
325 }
326 subscriptionbuffer.append(" </create-subscription>\n");
327 subscriptionbuffer.append("</rpc>\n");
328 subscriptionbuffer.append(ENDPATTERN);
329 return subscriptionbuffer.toString();
330 }
331
332 @Override
333 public void endSubscription() throws NetconfException {
334 if (subscriptionConnected) {
335 streamHandler.setEnableNotifications(false);
336 } else {
337 throw new NetconfException("Subscription does not exist.");
338 }
339 }
340
341 private void sendHello() throws NetconfException {
342 serverHelloResponseOld = sendRequest(createHelloString(), true);
343 Matcher capabilityMatcher = CAPABILITY_REGEX_PATTERN.matcher(serverHelloResponseOld);
344 while (capabilityMatcher.find()) {
345 deviceCapabilities.add(capabilityMatcher.group(1));
346 }
347 sessionID = String.valueOf(-1);
348 Matcher sessionIDMatcher = SESSION_ID_REGEX_PATTERN.matcher(serverHelloResponseOld);
349 if (sessionIDMatcher.find()) {
350 sessionID = sessionIDMatcher.group(1);
351 } else {
352 throw new NetconfException("Missing SessionID in server hello " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200353 "reponse.");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700354 }
355
356 }
357
358 private String createHelloString() {
359 StringBuilder hellobuffer = new StringBuilder();
360 hellobuffer.append(XML_HEADER);
361 hellobuffer.append("\n");
362 hellobuffer.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
363 hellobuffer.append(" <capabilities>\n");
364 onosCapabilities.forEach(
365 cap -> hellobuffer.append(" <capability>")
366 .append(cap)
367 .append("</capability>\n"));
368 hellobuffer.append(" </capabilities>\n");
369 hellobuffer.append("</hello>\n");
370 hellobuffer.append(ENDPATTERN);
371 return hellobuffer.toString();
372
373 }
374
375 @Override
376 public void checkAndReestablish() throws NetconfException {
377 try {
378 if (client.isClosed()) {
379 log.debug("Trying to restart the whole SSH connection with {}", deviceInfo.getDeviceId());
380 cleanUp();
381 startConnection();
382 } else if (session.isClosed()) {
383 log.debug("Trying to restart the session with {}", session, deviceInfo.getDeviceId());
384 cleanUp();
385 startSession();
386 } else if (channel.isClosed()) {
387 log.debug("Trying to reopen the channel with {}", deviceInfo.getDeviceId());
388 cleanUp();
389 openChannel();
390 }
391 if (subscriptionConnected) {
392 log.debug("Restarting subscription with {}", deviceInfo.getDeviceId());
393 subscriptionConnected = false;
394 startSubscription(notificationFilterSchema);
395 }
Sean Condon7347de92017-07-21 12:17:25 +0100396 } catch (IOException | IllegalStateException e) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700397 log.error("Can't reopen connection for device {}", e.getMessage());
398 throw new NetconfException("Cannot re-open the connection with device" + deviceInfo, e);
399 }
400 }
401
402 private void cleanUp() {
403 //makes sure everything is at a clean state.
404 replies.clear();
405 }
406
407 @Override
408 public String requestSync(String request) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700409 String reply = sendRequest(request);
410 checkReply(reply);
411 return reply;
412 }
413
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200414
415 /**
416 * Validate and format netconf message.
417 *
418 * @param message to format
419 * @return formated message
420 */
421 private String formatNetconfMessage(String message) {
422 if (deviceCapabilities.contains(NETCONF_11_CAPABILITY)) {
423 message = formatChunkedMessage(message);
424 } else {
425 if (!message.contains(ENDPATTERN)) {
426 message = message + NEW_LINE + ENDPATTERN;
427 }
428 }
429 return message;
430 }
431
432 /**
433 * Validate and format message according to chunked framing mechanism.
434 *
435 * @param message to format
436 * @return formated message
437 */
438 private String formatChunkedMessage(String message) {
439 if (message.endsWith(ENDPATTERN)) {
440 message = message.substring(0, message.length() - ENDPATTERN.length());
441 }
442 if (!message.startsWith(LF + HASH)) {
443 try {
444 message = LF + HASH + message.getBytes("UTF-8").length + LF + message + LF + HASH + HASH + LF;
445 } catch (UnsupportedEncodingException e) {
446 e.printStackTrace();
447 }
448 }
449 return message;
450 }
451
452
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700453 @Override
454 @Deprecated
455 public CompletableFuture<String> request(String request) {
456 return streamHandler.sendMessage(request);
457 }
458
459 private CompletableFuture<String> request(String request, int messageId) {
460 return streamHandler.sendMessage(request, messageId);
461 }
462
463 private String sendRequest(String request) throws NetconfException {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200464 request = formatNetconfMessage(request);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700465 return sendRequest(request, false);
466 }
467
468 private String sendRequest(String request, boolean isHello) throws NetconfException {
469 checkAndReestablish();
470 int messageId = -1;
471 if (!isHello) {
472 messageId = messageIdInteger.getAndIncrement();
473 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700474 request = formatXmlHeader(request);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200475 request = formatRequestMessageId(request, messageId);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700476 CompletableFuture<String> futureReply = request(request, messageId);
477 int replyTimeout = NetconfControllerImpl.netconfReplyTimeout;
478 String rp;
479 try {
480 rp = futureReply.get(replyTimeout, TimeUnit.SECONDS);
Sean Condon7347de92017-07-21 12:17:25 +0100481 replies.remove(messageId); // Why here???
482 } catch (InterruptedException e) {
483 Thread.currentThread().interrupt();
484 throw new NetconfException("Interrupted waiting for reply for request" + request, e);
485 } catch (TimeoutException e) {
486 throw new NetconfException("Timed out waiting for reply for request " + request, e);
487 } catch (ExecutionException e) {
488 log.warn("Closing session {} for {} due to unexpected Error", sessionID, deviceInfo, e);
489 try {
490 session.close();
491 channel.close(); //Closes the socket which should interrupt NetconfStreamThread
492 client.close();
493 } catch (IOException ioe) {
494 log.warn("Error closing session {} on {}", sessionID, deviceInfo, ioe);
495 }
496 NetconfDeviceOutputEvent event = new NetconfDeviceOutputEvent(
497 NetconfDeviceOutputEvent.Type.SESSION_CLOSED,
498 null, "Closed due to unexpected error " + e.getCause(),
499 Optional.of(-1), deviceInfo);
500 publishEvent(event);
501 errorReplies.clear(); // move to cleanUp()?
502 cleanUp();
503
504 throw new NetconfException("Closing session " + sessionID + " for " + deviceInfo +
505 " for request " + request, e);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700506 }
507 log.debug("Result {} from request {} to device {}", rp, request, deviceInfo);
508 return rp.trim();
509 }
510
511 private String formatRequestMessageId(String request, int messageId) {
512 if (request.contains(MESSAGE_ID_STRING)) {
513 //FIXME if application provides his own counting of messages this fails that count
514 request = request.replaceFirst(MESSAGE_ID_STRING + EQUAL + NUMBER_BETWEEN_QUOTES_MATCHER,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200515 MESSAGE_ID_STRING + EQUAL + "\"" + messageId + "\"");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700516 } else if (!request.contains(MESSAGE_ID_STRING) && !request.contains(HELLO)) {
517 //FIXME find out a better way to enforce the presence of message-id
518 request = request.replaceFirst(END_OF_RPC_OPEN_TAG, "\" " + MESSAGE_ID_STRING + EQUAL + "\""
519 + messageId + "\"" + ">");
520 }
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200521 request = updateRequestLenght(request);
522 return request;
523 }
524
525 private String updateRequestLenght(String request) {
526 if (request.contains(LF + HASH + HASH + LF)) {
527 int oldLen = Integer.parseInt(request.split(HASH)[1].split(LF)[0]);
528 String rpcWithEnding = request.substring(request.indexOf('<'));
529 String firstBlock = request.split(MSGLEN_REGEX_PATTERN)[1].split(LF + HASH + HASH + LF)[0];
530 int newLen = 0;
531 try {
532 newLen = firstBlock.getBytes("UTF-8").length;
533 } catch (UnsupportedEncodingException e) {
534 e.printStackTrace();
535 }
536 if (oldLen != newLen) {
537 return LF + HASH + newLen + LF + rpcWithEnding;
538 }
539 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700540 return request;
541 }
542
543 private String formatXmlHeader(String request) {
544 if (!request.contains(XML_HEADER)) {
545 //FIXME if application provieds his own XML header of different type there is a clash
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200546 if (request.startsWith(LF + HASH)) {
547 request = request.split("<")[0] + XML_HEADER + request.substring(request.split("<")[0].length());
548 } else {
549 request = XML_HEADER + "\n" + request;
550 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700551 }
552 return request;
553 }
554
555 @Override
556 public String doWrappedRpc(String request) throws NetconfException {
557 StringBuilder rpc = new StringBuilder(XML_HEADER);
558 rpc.append(RPC_OPEN);
559 rpc.append(MESSAGE_ID_STRING);
560 rpc.append(EQUAL);
561 rpc.append("\"");
562 rpc.append(messageIdInteger.get());
563 rpc.append("\" ");
564 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
565 rpc.append(request);
566 rpc.append(RPC_CLOSE).append(NEW_LINE);
567 rpc.append(ENDPATTERN);
568 String reply = sendRequest(rpc.toString());
569 checkReply(reply);
570 return reply;
571 }
572
573 @Override
574 public String get(String request) throws NetconfException {
575 return requestSync(request);
576 }
577
578 @Override
579 public String get(String filterSchema, String withDefaultsMode) throws NetconfException {
580 StringBuilder rpc = new StringBuilder(XML_HEADER);
581 rpc.append(RPC_OPEN);
582 rpc.append(MESSAGE_ID_STRING);
583 rpc.append(EQUAL);
584 rpc.append("\"");
585 rpc.append(messageIdInteger.get());
586 rpc.append("\" ");
587 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
588 rpc.append(GET_OPEN).append(NEW_LINE);
589 if (filterSchema != null) {
590 rpc.append(SUBTREE_FILTER_OPEN).append(NEW_LINE);
591 rpc.append(filterSchema).append(NEW_LINE);
592 rpc.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
593 }
594 if (withDefaultsMode != null) {
595 rpc.append(WITH_DEFAULT_OPEN).append(NETCONF_WITH_DEFAULTS_NAMESPACE).append(">");
596 rpc.append(withDefaultsMode).append(WITH_DEFAULT_CLOSE).append(NEW_LINE);
597 }
598 rpc.append(GET_CLOSE).append(NEW_LINE);
599 rpc.append(RPC_CLOSE).append(NEW_LINE);
600 rpc.append(ENDPATTERN);
601 String reply = sendRequest(rpc.toString());
602 checkReply(reply);
603 return reply;
604 }
605
606 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700607 public String getConfig(DatastoreId netconfTargetConfig) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700608 return getConfig(netconfTargetConfig, null);
609 }
610
611 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700612 public String getConfig(DatastoreId netconfTargetConfig,
613 String configurationSchema) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700614 StringBuilder rpc = new StringBuilder(XML_HEADER);
615 rpc.append("<rpc ");
616 rpc.append(MESSAGE_ID_STRING);
617 rpc.append(EQUAL);
618 rpc.append("\"");
619 rpc.append(messageIdInteger.get());
620 rpc.append("\" ");
621 rpc.append("xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
622 rpc.append("<get-config>\n");
623 rpc.append("<source>\n");
624 rpc.append("<").append(netconfTargetConfig).append("/>");
625 rpc.append("</source>");
626 if (configurationSchema != null) {
627 rpc.append("<filter type=\"subtree\">\n");
628 rpc.append(configurationSchema).append("\n");
629 rpc.append("</filter>\n");
630 }
631 rpc.append("</get-config>\n");
632 rpc.append("</rpc>\n");
633 rpc.append(ENDPATTERN);
634 String reply = sendRequest(rpc.toString());
635 return checkReply(reply) ? reply : "ERROR " + reply;
636 }
637
638 @Override
639 public boolean editConfig(String newConfiguration) throws NetconfException {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200640 if (!newConfiguration.endsWith(ENDPATTERN)) {
641 newConfiguration = newConfiguration + ENDPATTERN;
642 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700643 return checkReply(sendRequest(newConfiguration));
644 }
645
646 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700647 public boolean editConfig(DatastoreId netconfTargetConfig,
648 String mode,
649 String newConfiguration)
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700650 throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700651
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700652 newConfiguration = newConfiguration.trim();
653 StringBuilder rpc = new StringBuilder(XML_HEADER);
654 rpc.append(RPC_OPEN);
655 rpc.append(MESSAGE_ID_STRING);
656 rpc.append(EQUAL);
657 rpc.append("\"");
658 rpc.append(messageIdInteger.get());
659 rpc.append("\" ");
660 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
661 rpc.append(EDIT_CONFIG_OPEN).append("\n");
662 rpc.append(TARGET_OPEN);
663 rpc.append("<").append(netconfTargetConfig).append("/>");
664 rpc.append(TARGET_CLOSE).append("\n");
665 if (mode != null) {
666 rpc.append(DEFAULT_OPERATION_OPEN);
667 rpc.append(mode);
668 rpc.append(DEFAULT_OPERATION_CLOSE).append("\n");
669 }
670 rpc.append(CONFIG_OPEN).append("\n");
671 rpc.append(newConfiguration);
672 rpc.append(CONFIG_CLOSE).append("\n");
673 rpc.append(EDIT_CONFIG_CLOSE).append("\n");
674 rpc.append(RPC_CLOSE);
675 rpc.append(ENDPATTERN);
676 log.debug(rpc.toString());
677 String reply = sendRequest(rpc.toString());
678 return checkReply(reply);
679 }
680
681 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700682 public boolean copyConfig(DatastoreId destination,
683 DatastoreId source)
684 throws NetconfException {
685 return bareCopyConfig(destination.asXml(), source.asXml());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700686 }
687
688 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700689 public boolean copyConfig(DatastoreId netconfTargetConfig,
690 String newConfiguration)
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700691 throws NetconfException {
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700692 return bareCopyConfig(netconfTargetConfig.asXml(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200693 normalizeCopyConfigParam(newConfiguration));
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700694 }
695
696 @Override
697 public boolean copyConfig(String netconfTargetConfig,
698 String newConfiguration) throws NetconfException {
699 return bareCopyConfig(normalizeCopyConfigParam(netconfTargetConfig),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200700 normalizeCopyConfigParam(newConfiguration));
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700701 }
702
703 /**
704 * Normalize String parameter passed to copy-config API.
705 * <p>
706 * Provided for backward compatibility purpose
707 *
708 * @param input passed to copyConfig API
709 * @return XML likely to be suitable for copy-config source or target
710 */
711 private static CharSequence normalizeCopyConfigParam(String input) {
712 input = input.trim();
713 if (input.startsWith("<url")) {
714 return input;
715 } else if (!input.startsWith("<")) {
716 // assume it is a datastore name
717 return DatastoreId.datastore(input).asXml();
718 } else if (!input.startsWith("<config>")) {
719 return "<config>" + input + "</config>";
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700720 }
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700721 return input;
722 }
723
724 private boolean bareCopyConfig(CharSequence target,
725 CharSequence source)
726 throws NetconfException {
727
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700728 StringBuilder rpc = new StringBuilder(XML_HEADER);
729 rpc.append(RPC_OPEN);
730 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
731 rpc.append("<copy-config>");
732 rpc.append("<target>");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700733 rpc.append(target);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700734 rpc.append("</target>");
735 rpc.append("<source>");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700736 rpc.append(source);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700737 rpc.append("</source>");
738 rpc.append("</copy-config>");
739 rpc.append("</rpc>");
740 rpc.append(ENDPATTERN);
741 return checkReply(sendRequest(rpc.toString()));
742 }
743
744 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700745 public boolean deleteConfig(DatastoreId netconfTargetConfig) throws NetconfException {
746 if (netconfTargetConfig.equals(DatastoreId.RUNNING)) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700747 log.warn("Target configuration for delete operation can't be \"running\"",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200748 netconfTargetConfig);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700749 return false;
750 }
751 StringBuilder rpc = new StringBuilder(XML_HEADER);
752 rpc.append("<rpc>");
753 rpc.append("<delete-config>");
754 rpc.append("<target>");
755 rpc.append("<").append(netconfTargetConfig).append("/>");
756 rpc.append("</target>");
757 rpc.append("</delete-config>");
758 rpc.append("</rpc>");
759 rpc.append(ENDPATTERN);
760 return checkReply(sendRequest(rpc.toString()));
761 }
762
763 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700764 public boolean lock(DatastoreId configType) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700765 StringBuilder rpc = new StringBuilder(XML_HEADER);
766 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
767 rpc.append("<lock>");
768 rpc.append("<target>");
769 rpc.append("<");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700770 rpc.append(configType.id());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700771 rpc.append("/>");
772 rpc.append("</target>");
773 rpc.append("</lock>");
774 rpc.append("</rpc>");
775 rpc.append(ENDPATTERN);
776 String lockReply = sendRequest(rpc.toString());
777 return checkReply(lockReply);
778 }
779
780 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700781 public boolean unlock(DatastoreId configType) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700782 StringBuilder rpc = new StringBuilder(XML_HEADER);
783 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
784 rpc.append("<unlock>");
785 rpc.append("<target>");
786 rpc.append("<");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700787 rpc.append(configType.id());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700788 rpc.append("/>");
789 rpc.append("</target>");
790 rpc.append("</unlock>");
791 rpc.append("</rpc>");
792 rpc.append(ENDPATTERN);
793 String unlockReply = sendRequest(rpc.toString());
794 return checkReply(unlockReply);
795 }
796
797 @Override
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700798 public boolean close() throws NetconfException {
799 return close(false);
800 }
801
802 private boolean close(boolean force) throws NetconfException {
803 StringBuilder rpc = new StringBuilder();
804 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">");
805 if (force) {
806 rpc.append("<kill-session/>");
807 } else {
808 rpc.append("<close-session/>");
809 }
810 rpc.append("</rpc>");
811 rpc.append(ENDPATTERN);
812 return checkReply(sendRequest(rpc.toString())) || close(true);
813 }
814
815 @Override
816 public String getSessionId() {
817 return sessionID;
818 }
819
820 @Override
821 public Set<String> getDeviceCapabilitiesSet() {
822 return Collections.unmodifiableSet(deviceCapabilities);
823 }
824
825 @Deprecated
826 @Override
827 public String getServerCapabilities() {
828 return serverHelloResponseOld;
829 }
830
831 @Deprecated
832 @Override
833 public void setDeviceCapabilities(List<String> capabilities) {
834 onosCapabilities = capabilities;
835 }
836
837 @Override
838 public void setOnosCapabilities(Iterable<String> capabilities) {
839 onosCapabilities = capabilities;
840 }
841
842
843 @Override
844 public void addDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
845 streamHandler.addDeviceEventListener(listener);
846 primaryListeners.add(listener);
847 }
848
849 @Override
850 public void removeDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
851 primaryListeners.remove(listener);
852 streamHandler.removeDeviceEventListener(listener);
853 }
854
855 private boolean checkReply(String reply) throws NetconfException {
856 if (reply != null) {
857 if (!reply.contains("<rpc-error>")) {
858 log.debug("Device {} sent reply {}", deviceInfo, reply);
859 return true;
860 } else if (reply.contains("<ok/>")
861 || (reply.contains("<rpc-error>")
862 && reply.contains("warning"))) {
863 log.debug("Device {} sent reply {}", deviceInfo, reply);
864 return true;
865 }
866 }
867 log.warn("Device {} has error in reply {}", deviceInfo, reply);
868 return false;
869 }
870
Sean Condon7347de92017-07-21 12:17:25 +0100871 protected void publishEvent(NetconfDeviceOutputEvent event) {
872 primaryListeners.forEach(lsnr -> {
873 if (lsnr.isRelevant(event)) {
874 lsnr.event(event);
875 }
876 });
877 }
878
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700879 static class NotificationSession extends NetconfSessionMinaImpl {
880
881 private String notificationFilter;
882
883 NotificationSession(NetconfDeviceInfo deviceInfo)
884 throws NetconfException {
885 super(deviceInfo);
886 }
887
888 @Override
889 protected void startSubscriptionStream(String filterSchema)
890 throws NetconfException {
891
892 notificationFilter = filterSchema;
893 requestSync(createSubscriptionString(filterSchema));
894 }
895
896 @Override
897 public String toString() {
898 return MoreObjects.toStringHelper(getClass())
899 .add("deviceInfo", deviceInfo)
900 .add("sessionID", getSessionId())
901 .add("notificationFilter", notificationFilter)
902 .toString();
903 }
904 }
905
906 /**
907 * Listener attached to child session for notification streaming.
908 * <p>
909 * Forwards all notification event from child session to primary session
910 * listeners.
911 */
912 private final class NotificationForwarder
913 implements NetconfDeviceOutputEventListener {
914
915 @Override
916 public boolean isRelevant(NetconfDeviceOutputEvent event) {
917 return event.type() == Type.DEVICE_NOTIFICATION;
918 }
919
920 @Override
921 public void event(NetconfDeviceOutputEvent event) {
Sean Condon7347de92017-07-21 12:17:25 +0100922 publishEvent(event);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700923 }
924 }
925
926 public class NetconfSessionDelegateImpl implements NetconfSessionDelegate {
927
928 @Override
929 public void notify(NetconfDeviceOutputEvent event) {
930 Optional<Integer> messageId = event.getMessageID();
931 log.debug("messageID {}, waiting replies messageIDs {}", messageId,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200932 replies.keySet());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700933 if (!messageId.isPresent()) {
934 errorReplies.add(event.getMessagePayload());
935 log.error("Device {} sent error reply {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200936 event.getDeviceInfo(), event.getMessagePayload());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700937 return;
938 }
939 CompletableFuture<String> completedReply =
Sean Condon7347de92017-07-21 12:17:25 +0100940 replies.get(messageId.get()); // remove(..)?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700941 if (completedReply != null) {
942 completedReply.complete(event.getMessagePayload());
943 }
944 }
945 }
946
947 public static class MinaSshNetconfSessionFactory implements NetconfSessionFactory {
948
949 @Override
950 public NetconfSession createNetconfSession(NetconfDeviceInfo netconfDeviceInfo) throws NetconfException {
951 return new NetconfSessionMinaImpl(netconfDeviceInfo);
952 }
953 }
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200954}