blob: 6d30314ca503939b24e09f29e986a195a9f286ee [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
Sean Condon54d82432017-07-26 22:27:25 +0100146 private int connectTimeout;
147 private int replyTimeout;
148 private int idleTimeout;
149
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700150
151 private ClientChannel channel = null;
152 private ClientSession session = null;
153 private SshClient client = null;
154
155
156 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo) throws NetconfException {
157 this.deviceInfo = deviceInfo;
158 replies = new ConcurrentHashMap<>();
159 errorReplies = new ArrayList<>();
Sean Condon54d82432017-07-26 22:27:25 +0100160
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700161 startConnection();
162 }
163
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200164 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo, List<String> capabilities) throws NetconfException {
165 this.deviceInfo = deviceInfo;
166 replies = new ConcurrentHashMap<>();
167 errorReplies = new ArrayList<>();
168 setOnosCapabilities(capabilities);
169 startConnection();
170 }
171
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700172 private void startConnection() throws NetconfException {
Sean Condon54d82432017-07-26 22:27:25 +0100173 connectTimeout = deviceInfo.getConnectTimeoutSec().orElse(
174 NetconfControllerImpl.netconfConnectTimeout);
175 replyTimeout = deviceInfo.getReplyTimeoutSec().orElse(
176 NetconfControllerImpl.netconfReplyTimeout);
177 idleTimeout = deviceInfo.getIdleTimeoutSec().orElse(
178 NetconfControllerImpl.netconfIdleTimeout);
179 log.info("Connecting to {} with timeouts C:{}, R:{}, I:{}", deviceInfo,
180 connectTimeout, replyTimeout, idleTimeout);
181
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700182 try {
183 startClient();
184 } catch (IOException e) {
185 throw new NetconfException("Failed to establish SSH with device " + deviceInfo, e);
186 }
187 }
188
189 private void startClient() throws IOException {
190 client = SshClient.setUpDefaultClient();
Sean Condon7347de92017-07-21 12:17:25 +0100191 client.getProperties().putIfAbsent(FactoryManager.IDLE_TIMEOUT,
Sean Condon54d82432017-07-26 22:27:25 +0100192 TimeUnit.SECONDS.toMillis(idleTimeout));
Sean Condon7347de92017-07-21 12:17:25 +0100193 client.getProperties().putIfAbsent(FactoryManager.NIO2_READ_TIMEOUT,
Sean Condon54d82432017-07-26 22:27:25 +0100194 TimeUnit.SECONDS.toMillis(idleTimeout + 15L));
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700195 client.start();
196 client.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
197 startSession();
198 }
199
200 private void startSession() throws IOException {
201 final ConnectFuture connectFuture;
202 connectFuture = client.connect(deviceInfo.name(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200203 deviceInfo.ip().toString(),
204 deviceInfo.port())
Sean Condon54d82432017-07-26 22:27:25 +0100205 .verify(connectTimeout, TimeUnit.SECONDS);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700206 session = connectFuture.getSession();
207 //Using the device ssh key if possible
208 if (deviceInfo.getKey() != null) {
209 ByteBuffer buf = StandardCharsets.UTF_8.encode(CharBuffer.wrap(deviceInfo.getKey()));
210 byte[] byteKey = new byte[buf.limit()];
211 buf.get(byteKey);
212 PublicKey key;
213 try {
214 key = getPublicKey(byteKey, RSA);
215 } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
216 try {
217 key = getPublicKey(byteKey, DSA);
218 } catch (NoSuchAlgorithmException | InvalidKeySpecException e1) {
219 throw new NetconfException("Failed to authenticate session with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200220 deviceInfo + "check key to be the " +
221 "proper DSA or RSA key", e1);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700222 }
223 }
224 //privateKye can set tu null because is not used by the method.
225 session.addPublicKeyIdentity(new KeyPair(key, null));
226 } else {
227 session.addPasswordIdentity(deviceInfo.password());
228 }
Sean Condon54d82432017-07-26 22:27:25 +0100229 session.auth().verify(connectTimeout, TimeUnit.SECONDS);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700230 Set<ClientSession.ClientSessionEvent> event = session.waitFor(
231 ImmutableSet.of(ClientSession.ClientSessionEvent.WAIT_AUTH,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200232 ClientSession.ClientSessionEvent.CLOSED,
233 ClientSession.ClientSessionEvent.AUTHED), 0);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700234
235 if (!event.contains(ClientSession.ClientSessionEvent.AUTHED)) {
236 log.debug("Session closed {} {}", event, session.isClosed());
237 throw new NetconfException("Failed to authenticate session with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200238 deviceInfo + "check the user/pwd or key");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700239 }
240 openChannel();
241 }
242
243 private PublicKey getPublicKey(byte[] keyBytes, String type)
244 throws NoSuchAlgorithmException, InvalidKeySpecException {
245
246 X509EncodedKeySpec spec =
247 new X509EncodedKeySpec(keyBytes);
248 KeyFactory kf = KeyFactory.getInstance(type);
249 return kf.generatePublic(spec);
250 }
251
252 private void openChannel() throws IOException {
253 channel = session.createSubsystemChannel("netconf");
254 OpenFuture channelFuture = channel.open();
Sean Condon54d82432017-07-26 22:27:25 +0100255 if (channelFuture.await(connectTimeout, TimeUnit.SECONDS)) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700256 if (channelFuture.isOpened()) {
257 streamHandler = new NetconfStreamThread(channel.getInvertedOut(), channel.getInvertedIn(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200258 channel.getInvertedErr(), deviceInfo,
259 new NetconfSessionDelegateImpl(), replies);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700260 } else {
261 throw new NetconfException("Failed to open channel with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200262 deviceInfo);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700263 }
264 sendHello();
265 }
266 }
267
268
269 @Beta
270 protected void startSubscriptionStream(String filterSchema) throws NetconfException {
271 boolean openNewSession = false;
272 if (!deviceCapabilities.contains(INTERLEAVE_CAPABILITY_STRING)) {
273 log.info("Device {} doesn't support interleave, creating child session", deviceInfo);
274 openNewSession = true;
275
276 } else if (subscriptionConnected &&
277 notificationFilterSchema != null &&
278 !Objects.equal(filterSchema, notificationFilterSchema)) {
279 // interleave supported and existing filter is NOT "no filtering"
280 // and was requested with different filtering schema
281 log.info("Cannot use existing session for subscription {} ({})",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200282 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700283 openNewSession = true;
284 }
285
286 if (openNewSession) {
287 log.info("Creating notification session to {} with filter {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200288 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700289 NetconfSession child = new NotificationSession(deviceInfo);
290
291 child.addDeviceOutputListener(new NotificationForwarder());
292
293 child.startSubscription(filterSchema);
294 children.add(child);
295 return;
296 }
297
298 // request to start interleaved notification session
299 String reply = sendRequest(createSubscriptionString(filterSchema));
300 if (!checkReply(reply)) {
301 throw new NetconfException("Subscription not successful with device "
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200302 + deviceInfo + " with reply " + reply);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700303 }
304 subscriptionConnected = true;
305 }
306
307 @Override
308 public void startSubscription() throws NetconfException {
309 if (!subscriptionConnected) {
310 startSubscriptionStream(null);
311 }
312 streamHandler.setEnableNotifications(true);
313 }
314
315 @Beta
316 @Override
317 public void startSubscription(String filterSchema) throws NetconfException {
318 if (!subscriptionConnected) {
319 notificationFilterSchema = filterSchema;
320 startSubscriptionStream(filterSchema);
321 }
322 streamHandler.setEnableNotifications(true);
323 }
324
325 @Beta
326 protected String createSubscriptionString(String filterSchema) {
327 StringBuilder subscriptionbuffer = new StringBuilder();
328 subscriptionbuffer.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
329 subscriptionbuffer.append(" <create-subscription\n");
330 subscriptionbuffer.append("xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">\n");
331 // FIXME Only subtree filtering supported at the moment.
332 if (filterSchema != null) {
333 subscriptionbuffer.append(" ");
334 subscriptionbuffer.append(SUBSCRIPTION_SUBTREE_FILTER_OPEN).append(NEW_LINE);
335 subscriptionbuffer.append(filterSchema).append(NEW_LINE);
336 subscriptionbuffer.append(" ");
337 subscriptionbuffer.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
338 }
339 subscriptionbuffer.append(" </create-subscription>\n");
340 subscriptionbuffer.append("</rpc>\n");
341 subscriptionbuffer.append(ENDPATTERN);
342 return subscriptionbuffer.toString();
343 }
344
345 @Override
346 public void endSubscription() throws NetconfException {
347 if (subscriptionConnected) {
348 streamHandler.setEnableNotifications(false);
349 } else {
350 throw new NetconfException("Subscription does not exist.");
351 }
352 }
353
354 private void sendHello() throws NetconfException {
355 serverHelloResponseOld = sendRequest(createHelloString(), true);
356 Matcher capabilityMatcher = CAPABILITY_REGEX_PATTERN.matcher(serverHelloResponseOld);
357 while (capabilityMatcher.find()) {
358 deviceCapabilities.add(capabilityMatcher.group(1));
359 }
360 sessionID = String.valueOf(-1);
361 Matcher sessionIDMatcher = SESSION_ID_REGEX_PATTERN.matcher(serverHelloResponseOld);
362 if (sessionIDMatcher.find()) {
363 sessionID = sessionIDMatcher.group(1);
364 } else {
365 throw new NetconfException("Missing SessionID in server hello " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200366 "reponse.");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700367 }
368
369 }
370
371 private String createHelloString() {
372 StringBuilder hellobuffer = new StringBuilder();
373 hellobuffer.append(XML_HEADER);
374 hellobuffer.append("\n");
375 hellobuffer.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
376 hellobuffer.append(" <capabilities>\n");
377 onosCapabilities.forEach(
378 cap -> hellobuffer.append(" <capability>")
379 .append(cap)
380 .append("</capability>\n"));
381 hellobuffer.append(" </capabilities>\n");
382 hellobuffer.append("</hello>\n");
383 hellobuffer.append(ENDPATTERN);
384 return hellobuffer.toString();
385
386 }
387
388 @Override
389 public void checkAndReestablish() throws NetconfException {
390 try {
391 if (client.isClosed()) {
392 log.debug("Trying to restart the whole SSH connection with {}", deviceInfo.getDeviceId());
393 cleanUp();
394 startConnection();
395 } else if (session.isClosed()) {
396 log.debug("Trying to restart the session with {}", session, deviceInfo.getDeviceId());
397 cleanUp();
398 startSession();
399 } else if (channel.isClosed()) {
400 log.debug("Trying to reopen the channel with {}", deviceInfo.getDeviceId());
401 cleanUp();
402 openChannel();
403 }
404 if (subscriptionConnected) {
405 log.debug("Restarting subscription with {}", deviceInfo.getDeviceId());
406 subscriptionConnected = false;
407 startSubscription(notificationFilterSchema);
408 }
Sean Condon7347de92017-07-21 12:17:25 +0100409 } catch (IOException | IllegalStateException e) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700410 log.error("Can't reopen connection for device {}", e.getMessage());
411 throw new NetconfException("Cannot re-open the connection with device" + deviceInfo, e);
412 }
413 }
414
415 private void cleanUp() {
416 //makes sure everything is at a clean state.
417 replies.clear();
418 }
419
420 @Override
421 public String requestSync(String request) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700422 String reply = sendRequest(request);
423 checkReply(reply);
424 return reply;
425 }
426
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200427
428 /**
429 * Validate and format netconf message.
430 *
431 * @param message to format
432 * @return formated message
433 */
434 private String formatNetconfMessage(String message) {
435 if (deviceCapabilities.contains(NETCONF_11_CAPABILITY)) {
436 message = formatChunkedMessage(message);
437 } else {
438 if (!message.contains(ENDPATTERN)) {
439 message = message + NEW_LINE + ENDPATTERN;
440 }
441 }
442 return message;
443 }
444
445 /**
446 * Validate and format message according to chunked framing mechanism.
447 *
448 * @param message to format
449 * @return formated message
450 */
451 private String formatChunkedMessage(String message) {
452 if (message.endsWith(ENDPATTERN)) {
453 message = message.substring(0, message.length() - ENDPATTERN.length());
454 }
455 if (!message.startsWith(LF + HASH)) {
456 try {
457 message = LF + HASH + message.getBytes("UTF-8").length + LF + message + LF + HASH + HASH + LF;
458 } catch (UnsupportedEncodingException e) {
459 e.printStackTrace();
460 }
461 }
462 return message;
463 }
464
465
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700466 @Override
467 @Deprecated
468 public CompletableFuture<String> request(String request) {
469 return streamHandler.sendMessage(request);
470 }
471
Sean Condon54d82432017-07-26 22:27:25 +0100472 @Override
473 public int timeoutConnectSec() {
474 return connectTimeout;
475 }
476
477 @Override
478 public int timeoutReplySec() {
479 return replyTimeout;
480 }
481
482 @Override
483 public int timeoutIdleSec() {
484 return idleTimeout;
485 }
486
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700487 private CompletableFuture<String> request(String request, int messageId) {
488 return streamHandler.sendMessage(request, messageId);
489 }
490
491 private String sendRequest(String request) throws NetconfException {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200492 request = formatNetconfMessage(request);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700493 return sendRequest(request, false);
494 }
495
496 private String sendRequest(String request, boolean isHello) throws NetconfException {
497 checkAndReestablish();
498 int messageId = -1;
499 if (!isHello) {
500 messageId = messageIdInteger.getAndIncrement();
501 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700502 request = formatXmlHeader(request);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200503 request = formatRequestMessageId(request, messageId);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700504 CompletableFuture<String> futureReply = request(request, messageId);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700505 String rp;
506 try {
Sean Condon54d82432017-07-26 22:27:25 +0100507 log.debug("Sending request to NETCONF with timeout {} for {}",
508 replyTimeout, deviceInfo.name());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700509 rp = futureReply.get(replyTimeout, TimeUnit.SECONDS);
Sean Condon7347de92017-07-21 12:17:25 +0100510 replies.remove(messageId); // Why here???
511 } catch (InterruptedException e) {
512 Thread.currentThread().interrupt();
513 throw new NetconfException("Interrupted waiting for reply for request" + request, e);
514 } catch (TimeoutException e) {
Sean Condon54d82432017-07-26 22:27:25 +0100515 throw new NetconfException("Timed out waiting for reply for request " +
516 request + " after " + replyTimeout + " sec.", e);
Sean Condon7347de92017-07-21 12:17:25 +0100517 } catch (ExecutionException e) {
518 log.warn("Closing session {} for {} due to unexpected Error", sessionID, deviceInfo, e);
519 try {
520 session.close();
521 channel.close(); //Closes the socket which should interrupt NetconfStreamThread
522 client.close();
523 } catch (IOException ioe) {
524 log.warn("Error closing session {} on {}", sessionID, deviceInfo, ioe);
525 }
526 NetconfDeviceOutputEvent event = new NetconfDeviceOutputEvent(
527 NetconfDeviceOutputEvent.Type.SESSION_CLOSED,
528 null, "Closed due to unexpected error " + e.getCause(),
529 Optional.of(-1), deviceInfo);
530 publishEvent(event);
531 errorReplies.clear(); // move to cleanUp()?
532 cleanUp();
533
534 throw new NetconfException("Closing session " + sessionID + " for " + deviceInfo +
535 " for request " + request, e);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700536 }
537 log.debug("Result {} from request {} to device {}", rp, request, deviceInfo);
538 return rp.trim();
539 }
540
541 private String formatRequestMessageId(String request, int messageId) {
542 if (request.contains(MESSAGE_ID_STRING)) {
543 //FIXME if application provides his own counting of messages this fails that count
544 request = request.replaceFirst(MESSAGE_ID_STRING + EQUAL + NUMBER_BETWEEN_QUOTES_MATCHER,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200545 MESSAGE_ID_STRING + EQUAL + "\"" + messageId + "\"");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700546 } else if (!request.contains(MESSAGE_ID_STRING) && !request.contains(HELLO)) {
547 //FIXME find out a better way to enforce the presence of message-id
548 request = request.replaceFirst(END_OF_RPC_OPEN_TAG, "\" " + MESSAGE_ID_STRING + EQUAL + "\""
549 + messageId + "\"" + ">");
550 }
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200551 request = updateRequestLenght(request);
552 return request;
553 }
554
555 private String updateRequestLenght(String request) {
556 if (request.contains(LF + HASH + HASH + LF)) {
557 int oldLen = Integer.parseInt(request.split(HASH)[1].split(LF)[0]);
558 String rpcWithEnding = request.substring(request.indexOf('<'));
559 String firstBlock = request.split(MSGLEN_REGEX_PATTERN)[1].split(LF + HASH + HASH + LF)[0];
560 int newLen = 0;
561 try {
562 newLen = firstBlock.getBytes("UTF-8").length;
563 } catch (UnsupportedEncodingException e) {
564 e.printStackTrace();
565 }
566 if (oldLen != newLen) {
567 return LF + HASH + newLen + LF + rpcWithEnding;
568 }
569 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700570 return request;
571 }
572
573 private String formatXmlHeader(String request) {
574 if (!request.contains(XML_HEADER)) {
575 //FIXME if application provieds his own XML header of different type there is a clash
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200576 if (request.startsWith(LF + HASH)) {
577 request = request.split("<")[0] + XML_HEADER + request.substring(request.split("<")[0].length());
578 } else {
579 request = XML_HEADER + "\n" + request;
580 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700581 }
582 return request;
583 }
584
585 @Override
586 public String doWrappedRpc(String request) throws NetconfException {
587 StringBuilder rpc = new StringBuilder(XML_HEADER);
588 rpc.append(RPC_OPEN);
589 rpc.append(MESSAGE_ID_STRING);
590 rpc.append(EQUAL);
591 rpc.append("\"");
592 rpc.append(messageIdInteger.get());
593 rpc.append("\" ");
594 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
595 rpc.append(request);
596 rpc.append(RPC_CLOSE).append(NEW_LINE);
597 rpc.append(ENDPATTERN);
598 String reply = sendRequest(rpc.toString());
599 checkReply(reply);
600 return reply;
601 }
602
603 @Override
604 public String get(String request) throws NetconfException {
605 return requestSync(request);
606 }
607
608 @Override
609 public String get(String filterSchema, String withDefaultsMode) throws NetconfException {
610 StringBuilder rpc = new StringBuilder(XML_HEADER);
611 rpc.append(RPC_OPEN);
612 rpc.append(MESSAGE_ID_STRING);
613 rpc.append(EQUAL);
614 rpc.append("\"");
615 rpc.append(messageIdInteger.get());
616 rpc.append("\" ");
617 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
618 rpc.append(GET_OPEN).append(NEW_LINE);
619 if (filterSchema != null) {
620 rpc.append(SUBTREE_FILTER_OPEN).append(NEW_LINE);
621 rpc.append(filterSchema).append(NEW_LINE);
622 rpc.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
623 }
624 if (withDefaultsMode != null) {
625 rpc.append(WITH_DEFAULT_OPEN).append(NETCONF_WITH_DEFAULTS_NAMESPACE).append(">");
626 rpc.append(withDefaultsMode).append(WITH_DEFAULT_CLOSE).append(NEW_LINE);
627 }
628 rpc.append(GET_CLOSE).append(NEW_LINE);
629 rpc.append(RPC_CLOSE).append(NEW_LINE);
630 rpc.append(ENDPATTERN);
631 String reply = sendRequest(rpc.toString());
632 checkReply(reply);
633 return reply;
634 }
635
636 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700637 public String getConfig(DatastoreId netconfTargetConfig) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700638 return getConfig(netconfTargetConfig, null);
639 }
640
641 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700642 public String getConfig(DatastoreId netconfTargetConfig,
643 String configurationSchema) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700644 StringBuilder rpc = new StringBuilder(XML_HEADER);
645 rpc.append("<rpc ");
646 rpc.append(MESSAGE_ID_STRING);
647 rpc.append(EQUAL);
648 rpc.append("\"");
649 rpc.append(messageIdInteger.get());
650 rpc.append("\" ");
651 rpc.append("xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
652 rpc.append("<get-config>\n");
653 rpc.append("<source>\n");
654 rpc.append("<").append(netconfTargetConfig).append("/>");
655 rpc.append("</source>");
656 if (configurationSchema != null) {
657 rpc.append("<filter type=\"subtree\">\n");
658 rpc.append(configurationSchema).append("\n");
659 rpc.append("</filter>\n");
660 }
661 rpc.append("</get-config>\n");
662 rpc.append("</rpc>\n");
663 rpc.append(ENDPATTERN);
664 String reply = sendRequest(rpc.toString());
665 return checkReply(reply) ? reply : "ERROR " + reply;
666 }
667
668 @Override
669 public boolean editConfig(String newConfiguration) throws NetconfException {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200670 if (!newConfiguration.endsWith(ENDPATTERN)) {
671 newConfiguration = newConfiguration + ENDPATTERN;
672 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700673 return checkReply(sendRequest(newConfiguration));
674 }
675
676 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700677 public boolean editConfig(DatastoreId netconfTargetConfig,
678 String mode,
679 String newConfiguration)
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700680 throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700681
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700682 newConfiguration = newConfiguration.trim();
683 StringBuilder rpc = new StringBuilder(XML_HEADER);
684 rpc.append(RPC_OPEN);
685 rpc.append(MESSAGE_ID_STRING);
686 rpc.append(EQUAL);
687 rpc.append("\"");
688 rpc.append(messageIdInteger.get());
689 rpc.append("\" ");
690 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
691 rpc.append(EDIT_CONFIG_OPEN).append("\n");
692 rpc.append(TARGET_OPEN);
693 rpc.append("<").append(netconfTargetConfig).append("/>");
694 rpc.append(TARGET_CLOSE).append("\n");
695 if (mode != null) {
696 rpc.append(DEFAULT_OPERATION_OPEN);
697 rpc.append(mode);
698 rpc.append(DEFAULT_OPERATION_CLOSE).append("\n");
699 }
700 rpc.append(CONFIG_OPEN).append("\n");
701 rpc.append(newConfiguration);
702 rpc.append(CONFIG_CLOSE).append("\n");
703 rpc.append(EDIT_CONFIG_CLOSE).append("\n");
704 rpc.append(RPC_CLOSE);
705 rpc.append(ENDPATTERN);
706 log.debug(rpc.toString());
707 String reply = sendRequest(rpc.toString());
708 return checkReply(reply);
709 }
710
711 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700712 public boolean copyConfig(DatastoreId destination,
713 DatastoreId source)
714 throws NetconfException {
715 return bareCopyConfig(destination.asXml(), source.asXml());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700716 }
717
718 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700719 public boolean copyConfig(DatastoreId netconfTargetConfig,
720 String newConfiguration)
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700721 throws NetconfException {
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700722 return bareCopyConfig(netconfTargetConfig.asXml(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200723 normalizeCopyConfigParam(newConfiguration));
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700724 }
725
726 @Override
727 public boolean copyConfig(String netconfTargetConfig,
728 String newConfiguration) throws NetconfException {
729 return bareCopyConfig(normalizeCopyConfigParam(netconfTargetConfig),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200730 normalizeCopyConfigParam(newConfiguration));
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700731 }
732
733 /**
734 * Normalize String parameter passed to copy-config API.
735 * <p>
736 * Provided for backward compatibility purpose
737 *
738 * @param input passed to copyConfig API
739 * @return XML likely to be suitable for copy-config source or target
740 */
741 private static CharSequence normalizeCopyConfigParam(String input) {
742 input = input.trim();
743 if (input.startsWith("<url")) {
744 return input;
745 } else if (!input.startsWith("<")) {
746 // assume it is a datastore name
747 return DatastoreId.datastore(input).asXml();
748 } else if (!input.startsWith("<config>")) {
749 return "<config>" + input + "</config>";
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700750 }
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700751 return input;
752 }
753
754 private boolean bareCopyConfig(CharSequence target,
755 CharSequence source)
756 throws NetconfException {
757
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700758 StringBuilder rpc = new StringBuilder(XML_HEADER);
759 rpc.append(RPC_OPEN);
760 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
761 rpc.append("<copy-config>");
762 rpc.append("<target>");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700763 rpc.append(target);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700764 rpc.append("</target>");
765 rpc.append("<source>");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700766 rpc.append(source);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700767 rpc.append("</source>");
768 rpc.append("</copy-config>");
769 rpc.append("</rpc>");
770 rpc.append(ENDPATTERN);
771 return checkReply(sendRequest(rpc.toString()));
772 }
773
774 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700775 public boolean deleteConfig(DatastoreId netconfTargetConfig) throws NetconfException {
776 if (netconfTargetConfig.equals(DatastoreId.RUNNING)) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700777 log.warn("Target configuration for delete operation can't be \"running\"",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200778 netconfTargetConfig);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700779 return false;
780 }
781 StringBuilder rpc = new StringBuilder(XML_HEADER);
782 rpc.append("<rpc>");
783 rpc.append("<delete-config>");
784 rpc.append("<target>");
785 rpc.append("<").append(netconfTargetConfig).append("/>");
786 rpc.append("</target>");
787 rpc.append("</delete-config>");
788 rpc.append("</rpc>");
789 rpc.append(ENDPATTERN);
790 return checkReply(sendRequest(rpc.toString()));
791 }
792
793 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700794 public boolean lock(DatastoreId configType) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700795 StringBuilder rpc = new StringBuilder(XML_HEADER);
796 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
797 rpc.append("<lock>");
798 rpc.append("<target>");
799 rpc.append("<");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700800 rpc.append(configType.id());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700801 rpc.append("/>");
802 rpc.append("</target>");
803 rpc.append("</lock>");
804 rpc.append("</rpc>");
805 rpc.append(ENDPATTERN);
806 String lockReply = sendRequest(rpc.toString());
807 return checkReply(lockReply);
808 }
809
810 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700811 public boolean unlock(DatastoreId configType) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700812 StringBuilder rpc = new StringBuilder(XML_HEADER);
813 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
814 rpc.append("<unlock>");
815 rpc.append("<target>");
816 rpc.append("<");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700817 rpc.append(configType.id());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700818 rpc.append("/>");
819 rpc.append("</target>");
820 rpc.append("</unlock>");
821 rpc.append("</rpc>");
822 rpc.append(ENDPATTERN);
823 String unlockReply = sendRequest(rpc.toString());
824 return checkReply(unlockReply);
825 }
826
827 @Override
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700828 public boolean close() throws NetconfException {
829 return close(false);
830 }
831
832 private boolean close(boolean force) throws NetconfException {
833 StringBuilder rpc = new StringBuilder();
834 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">");
835 if (force) {
836 rpc.append("<kill-session/>");
837 } else {
838 rpc.append("<close-session/>");
839 }
840 rpc.append("</rpc>");
841 rpc.append(ENDPATTERN);
842 return checkReply(sendRequest(rpc.toString())) || close(true);
843 }
844
845 @Override
846 public String getSessionId() {
847 return sessionID;
848 }
849
850 @Override
851 public Set<String> getDeviceCapabilitiesSet() {
852 return Collections.unmodifiableSet(deviceCapabilities);
853 }
854
855 @Deprecated
856 @Override
857 public String getServerCapabilities() {
858 return serverHelloResponseOld;
859 }
860
861 @Deprecated
862 @Override
863 public void setDeviceCapabilities(List<String> capabilities) {
864 onosCapabilities = capabilities;
865 }
866
867 @Override
868 public void setOnosCapabilities(Iterable<String> capabilities) {
869 onosCapabilities = capabilities;
870 }
871
872
873 @Override
874 public void addDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
875 streamHandler.addDeviceEventListener(listener);
876 primaryListeners.add(listener);
877 }
878
879 @Override
880 public void removeDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
881 primaryListeners.remove(listener);
882 streamHandler.removeDeviceEventListener(listener);
883 }
884
885 private boolean checkReply(String reply) throws NetconfException {
886 if (reply != null) {
887 if (!reply.contains("<rpc-error>")) {
888 log.debug("Device {} sent reply {}", deviceInfo, reply);
889 return true;
890 } else if (reply.contains("<ok/>")
891 || (reply.contains("<rpc-error>")
892 && reply.contains("warning"))) {
893 log.debug("Device {} sent reply {}", deviceInfo, reply);
894 return true;
895 }
896 }
897 log.warn("Device {} has error in reply {}", deviceInfo, reply);
898 return false;
899 }
900
Sean Condon7347de92017-07-21 12:17:25 +0100901 protected void publishEvent(NetconfDeviceOutputEvent event) {
902 primaryListeners.forEach(lsnr -> {
903 if (lsnr.isRelevant(event)) {
904 lsnr.event(event);
905 }
906 });
907 }
908
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700909 static class NotificationSession extends NetconfSessionMinaImpl {
910
911 private String notificationFilter;
912
913 NotificationSession(NetconfDeviceInfo deviceInfo)
914 throws NetconfException {
915 super(deviceInfo);
916 }
917
918 @Override
919 protected void startSubscriptionStream(String filterSchema)
920 throws NetconfException {
921
922 notificationFilter = filterSchema;
923 requestSync(createSubscriptionString(filterSchema));
924 }
925
926 @Override
927 public String toString() {
928 return MoreObjects.toStringHelper(getClass())
929 .add("deviceInfo", deviceInfo)
930 .add("sessionID", getSessionId())
931 .add("notificationFilter", notificationFilter)
932 .toString();
933 }
934 }
935
936 /**
937 * Listener attached to child session for notification streaming.
938 * <p>
939 * Forwards all notification event from child session to primary session
940 * listeners.
941 */
942 private final class NotificationForwarder
943 implements NetconfDeviceOutputEventListener {
944
945 @Override
946 public boolean isRelevant(NetconfDeviceOutputEvent event) {
947 return event.type() == Type.DEVICE_NOTIFICATION;
948 }
949
950 @Override
951 public void event(NetconfDeviceOutputEvent event) {
Sean Condon7347de92017-07-21 12:17:25 +0100952 publishEvent(event);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700953 }
954 }
955
956 public class NetconfSessionDelegateImpl implements NetconfSessionDelegate {
957
958 @Override
959 public void notify(NetconfDeviceOutputEvent event) {
960 Optional<Integer> messageId = event.getMessageID();
961 log.debug("messageID {}, waiting replies messageIDs {}", messageId,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200962 replies.keySet());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700963 if (!messageId.isPresent()) {
964 errorReplies.add(event.getMessagePayload());
965 log.error("Device {} sent error reply {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200966 event.getDeviceInfo(), event.getMessagePayload());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700967 return;
968 }
969 CompletableFuture<String> completedReply =
Sean Condon7347de92017-07-21 12:17:25 +0100970 replies.get(messageId.get()); // remove(..)?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700971 if (completedReply != null) {
972 completedReply.complete(event.getMessagePayload());
973 }
974 }
975 }
976
977 public static class MinaSshNetconfSessionFactory implements NetconfSessionFactory {
978
979 @Override
980 public NetconfSession createNetconfSession(NetconfDeviceInfo netconfDeviceInfo) throws NetconfException {
981 return new NetconfSessionMinaImpl(netconfDeviceInfo);
982 }
983 }
Sean Condon54d82432017-07-26 22:27:25 +0100984}