blob: c8a4604eddce6ce59aff1c4f1dcd257b4b9d4be2 [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;
Holger Schulz092cbbf2017-08-31 17:52:30 +020031import org.bouncycastle.jce.provider.BouncyCastleProvider;
32import org.bouncycastle.openssl.PEMParser;
33import org.bouncycastle.openssl.PEMKeyPair;
34import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -070035import org.onosproject.netconf.DatastoreId;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070036import org.onosproject.netconf.NetconfDeviceInfo;
37import org.onosproject.netconf.NetconfDeviceOutputEvent;
38import org.onosproject.netconf.NetconfDeviceOutputEvent.Type;
39import org.onosproject.netconf.NetconfDeviceOutputEventListener;
40import org.onosproject.netconf.NetconfException;
41import org.onosproject.netconf.NetconfSession;
42import org.onosproject.netconf.NetconfSessionFactory;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070043import org.slf4j.Logger;
44import org.slf4j.LoggerFactory;
Yuta HIGUCHI15677982017-08-16 15:50:29 -070045
Holger Schulz092cbbf2017-08-31 17:52:30 +020046
Yuta HIGUCHI15677982017-08-16 15:50:29 -070047import static java.nio.charset.StandardCharsets.UTF_8;
48
Holger Schulz092cbbf2017-08-31 17:52:30 +020049import java.io.CharArrayReader;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070050import java.io.IOException;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070051import java.security.KeyFactory;
52import java.security.KeyPair;
53import java.security.NoSuchAlgorithmException;
54import java.security.PublicKey;
55import java.security.spec.InvalidKeySpecException;
56import java.security.spec.X509EncodedKeySpec;
Kamil Stasiak9f59f442017-05-02 11:02:24 +020057import java.util.LinkedHashSet;
58import java.util.Map;
59import java.util.Set;
60import java.util.List;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070061import java.util.Collection;
62import java.util.Collections;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070063import java.util.Optional;
Kamil Stasiak9f59f442017-05-02 11:02:24 +020064import java.util.ArrayList;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070065import java.util.concurrent.CompletableFuture;
66import java.util.concurrent.ConcurrentHashMap;
67import java.util.concurrent.CopyOnWriteArrayList;
68import java.util.concurrent.ExecutionException;
69import java.util.concurrent.TimeUnit;
70import java.util.concurrent.TimeoutException;
71import java.util.concurrent.atomic.AtomicInteger;
72import java.util.regex.Matcher;
73import java.util.regex.Pattern;
74
75/**
76 * Implementation of a NETCONF session to talk to a device.
77 */
78public class NetconfSessionMinaImpl implements NetconfSession {
79
80 private static final Logger log = LoggerFactory
81 .getLogger(NetconfSessionMinaImpl.class);
82
83 private static final String ENDPATTERN = "]]>]]>";
84 private static final String MESSAGE_ID_STRING = "message-id";
85 private static final String HELLO = "<hello";
86 private static final String NEW_LINE = "\n";
87 private static final String END_OF_RPC_OPEN_TAG = "\">";
88 private static final String EQUAL = "=";
89 private static final String NUMBER_BETWEEN_QUOTES_MATCHER = "\"+([0-9]+)+\"";
90 private static final String RPC_OPEN = "<rpc ";
91 private static final String RPC_CLOSE = "</rpc>";
92 private static final String GET_OPEN = "<get>";
93 private static final String GET_CLOSE = "</get>";
94 private static final String WITH_DEFAULT_OPEN = "<with-defaults ";
95 private static final String WITH_DEFAULT_CLOSE = "</with-defaults>";
96 private static final String DEFAULT_OPERATION_OPEN = "<default-operation>";
97 private static final String DEFAULT_OPERATION_CLOSE = "</default-operation>";
98 private static final String SUBTREE_FILTER_OPEN = "<filter type=\"subtree\">";
99 private static final String SUBTREE_FILTER_CLOSE = "</filter>";
100 private static final String EDIT_CONFIG_OPEN = "<edit-config>";
101 private static final String EDIT_CONFIG_CLOSE = "</edit-config>";
102 private static final String TARGET_OPEN = "<target>";
103 private static final String TARGET_CLOSE = "</target>";
104 private static final String CONFIG_OPEN = "<config xmlns:nc=\"urn:ietf:params:xml:ns:netconf:base:1.0\">";
105 private static final String CONFIG_CLOSE = "</config>";
106 private static final String XML_HEADER =
107 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
108 private static final String NETCONF_BASE_NAMESPACE =
109 "xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\"";
110 private static final String NETCONF_WITH_DEFAULTS_NAMESPACE =
111 "xmlns=\"urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults\"";
112 private static final String SUBSCRIPTION_SUBTREE_FILTER_OPEN =
113 "<filter xmlns:base10=\"urn:ietf:params:xml:ns:netconf:base:1.0\" base10:type=\"subtree\">";
114
115 private static final String INTERLEAVE_CAPABILITY_STRING = "urn:ietf:params:netconf:capability:interleave:1.0";
116
117 private static final String CAPABILITY_REGEX = "<capability>\\s*(.*?)\\s*</capability>";
118 private static final Pattern CAPABILITY_REGEX_PATTERN = Pattern.compile(CAPABILITY_REGEX);
119
120 private static final String SESSION_ID_REGEX = "<session-id>\\s*(.*?)\\s*</session-id>";
121 private static final Pattern SESSION_ID_REGEX_PATTERN = Pattern.compile(SESSION_ID_REGEX);
122 private static final String RSA = "RSA";
123 private static final String DSA = "DSA";
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200124 private static final String HASH = "#";
125 private static final String LF = "\n";
126 private static final String MSGLEN_REGEX_PATTERN = "\n#\\d+\n";
127 private static final String NETCONF_10_CAPABILITY = "urn:ietf:params:netconf:base:1.0";
128 private static final String NETCONF_11_CAPABILITY = "urn:ietf:params:netconf:base:1.1";
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700129
130 private String sessionID;
131 private final AtomicInteger messageIdInteger = new AtomicInteger(1);
132 protected final NetconfDeviceInfo deviceInfo;
133 private Iterable<String> onosCapabilities =
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200134 ImmutableList.of(NETCONF_10_CAPABILITY, NETCONF_11_CAPABILITY);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700135
136 /* NOTE: the "serverHelloResponseOld" is deprecated in 1.10.0 and should eventually be removed */
137 @Deprecated
138 private String serverHelloResponseOld;
139 private final Set<String> deviceCapabilities = new LinkedHashSet<>();
140 private NetconfStreamHandler streamHandler;
141 private Map<Integer, CompletableFuture<String>> replies;
Sean Condon7347de92017-07-21 12:17:25 +0100142 private List<String> errorReplies; // Not sure why we need this?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700143 private boolean subscriptionConnected = false;
144 private String notificationFilterSchema = null;
145
146 private final Collection<NetconfDeviceOutputEventListener> primaryListeners =
147 new CopyOnWriteArrayList<>();
148 private final Collection<NetconfSession> children =
149 new CopyOnWriteArrayList<>();
150
Sean Condon54d82432017-07-26 22:27:25 +0100151 private int connectTimeout;
152 private int replyTimeout;
153 private int idleTimeout;
154
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700155
156 private ClientChannel channel = null;
157 private ClientSession session = null;
158 private SshClient client = null;
159
160
161 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo) throws NetconfException {
162 this.deviceInfo = deviceInfo;
163 replies = new ConcurrentHashMap<>();
164 errorReplies = new ArrayList<>();
Sean Condon54d82432017-07-26 22:27:25 +0100165
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700166 startConnection();
167 }
168
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200169 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo, List<String> capabilities) throws NetconfException {
170 this.deviceInfo = deviceInfo;
171 replies = new ConcurrentHashMap<>();
172 errorReplies = new ArrayList<>();
173 setOnosCapabilities(capabilities);
174 startConnection();
175 }
176
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700177 private void startConnection() throws NetconfException {
Sean Condon54d82432017-07-26 22:27:25 +0100178 connectTimeout = deviceInfo.getConnectTimeoutSec().orElse(
179 NetconfControllerImpl.netconfConnectTimeout);
180 replyTimeout = deviceInfo.getReplyTimeoutSec().orElse(
181 NetconfControllerImpl.netconfReplyTimeout);
182 idleTimeout = deviceInfo.getIdleTimeoutSec().orElse(
183 NetconfControllerImpl.netconfIdleTimeout);
184 log.info("Connecting to {} with timeouts C:{}, R:{}, I:{}", deviceInfo,
185 connectTimeout, replyTimeout, idleTimeout);
186
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700187 try {
188 startClient();
189 } catch (IOException e) {
190 throw new NetconfException("Failed to establish SSH with device " + deviceInfo, e);
191 }
192 }
193
194 private void startClient() throws IOException {
195 client = SshClient.setUpDefaultClient();
Sean Condon7347de92017-07-21 12:17:25 +0100196 client.getProperties().putIfAbsent(FactoryManager.IDLE_TIMEOUT,
Sean Condon54d82432017-07-26 22:27:25 +0100197 TimeUnit.SECONDS.toMillis(idleTimeout));
Sean Condon7347de92017-07-21 12:17:25 +0100198 client.getProperties().putIfAbsent(FactoryManager.NIO2_READ_TIMEOUT,
Sean Condon54d82432017-07-26 22:27:25 +0100199 TimeUnit.SECONDS.toMillis(idleTimeout + 15L));
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700200 client.start();
201 client.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
202 startSession();
203 }
204
205 private void startSession() throws IOException {
206 final ConnectFuture connectFuture;
207 connectFuture = client.connect(deviceInfo.name(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200208 deviceInfo.ip().toString(),
209 deviceInfo.port())
Sean Condon54d82432017-07-26 22:27:25 +0100210 .verify(connectTimeout, TimeUnit.SECONDS);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700211 session = connectFuture.getSession();
212 //Using the device ssh key if possible
213 if (deviceInfo.getKey() != null) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700214 try (PEMParser pemParser = new PEMParser(new CharArrayReader(deviceInfo.getKey()))) {
215 JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME);
216 try {
217 KeyPair kp = converter.getKeyPair((PEMKeyPair) pemParser.readObject());
218 session.addPublicKeyIdentity(kp);
219 } catch (IOException e) {
220 throw new NetconfException("Failed to authenticate session with device " +
221 deviceInfo + "check key to be a valid key", e);
222 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700223 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700224 } else {
225 session.addPasswordIdentity(deviceInfo.password());
226 }
Sean Condon54d82432017-07-26 22:27:25 +0100227 session.auth().verify(connectTimeout, TimeUnit.SECONDS);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700228 Set<ClientSession.ClientSessionEvent> event = session.waitFor(
229 ImmutableSet.of(ClientSession.ClientSessionEvent.WAIT_AUTH,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200230 ClientSession.ClientSessionEvent.CLOSED,
231 ClientSession.ClientSessionEvent.AUTHED), 0);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700232
233 if (!event.contains(ClientSession.ClientSessionEvent.AUTHED)) {
234 log.debug("Session closed {} {}", event, session.isClosed());
235 throw new NetconfException("Failed to authenticate session with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200236 deviceInfo + "check the user/pwd or key");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700237 }
238 openChannel();
239 }
240
241 private PublicKey getPublicKey(byte[] keyBytes, String type)
242 throws NoSuchAlgorithmException, InvalidKeySpecException {
243
244 X509EncodedKeySpec spec =
245 new X509EncodedKeySpec(keyBytes);
246 KeyFactory kf = KeyFactory.getInstance(type);
247 return kf.generatePublic(spec);
248 }
249
250 private void openChannel() throws IOException {
251 channel = session.createSubsystemChannel("netconf");
252 OpenFuture channelFuture = channel.open();
Sean Condon54d82432017-07-26 22:27:25 +0100253 if (channelFuture.await(connectTimeout, TimeUnit.SECONDS)) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700254 if (channelFuture.isOpened()) {
255 streamHandler = new NetconfStreamThread(channel.getInvertedOut(), channel.getInvertedIn(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200256 channel.getInvertedErr(), deviceInfo,
257 new NetconfSessionDelegateImpl(), replies);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700258 } else {
259 throw new NetconfException("Failed to open channel with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200260 deviceInfo);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700261 }
262 sendHello();
263 }
264 }
265
266
267 @Beta
268 protected void startSubscriptionStream(String filterSchema) throws NetconfException {
269 boolean openNewSession = false;
270 if (!deviceCapabilities.contains(INTERLEAVE_CAPABILITY_STRING)) {
271 log.info("Device {} doesn't support interleave, creating child session", deviceInfo);
272 openNewSession = true;
273
274 } else if (subscriptionConnected &&
275 notificationFilterSchema != null &&
276 !Objects.equal(filterSchema, notificationFilterSchema)) {
277 // interleave supported and existing filter is NOT "no filtering"
278 // and was requested with different filtering schema
279 log.info("Cannot use existing session for subscription {} ({})",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200280 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700281 openNewSession = true;
282 }
283
284 if (openNewSession) {
285 log.info("Creating notification session to {} with filter {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200286 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700287 NetconfSession child = new NotificationSession(deviceInfo);
288
289 child.addDeviceOutputListener(new NotificationForwarder());
290
291 child.startSubscription(filterSchema);
292 children.add(child);
293 return;
294 }
295
296 // request to start interleaved notification session
297 String reply = sendRequest(createSubscriptionString(filterSchema));
298 if (!checkReply(reply)) {
299 throw new NetconfException("Subscription not successful with device "
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200300 + deviceInfo + " with reply " + reply);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700301 }
302 subscriptionConnected = true;
303 }
304
305 @Override
306 public void startSubscription() throws NetconfException {
307 if (!subscriptionConnected) {
308 startSubscriptionStream(null);
309 }
310 streamHandler.setEnableNotifications(true);
311 }
312
313 @Beta
314 @Override
315 public void startSubscription(String filterSchema) throws NetconfException {
316 if (!subscriptionConnected) {
317 notificationFilterSchema = filterSchema;
318 startSubscriptionStream(filterSchema);
319 }
320 streamHandler.setEnableNotifications(true);
321 }
322
323 @Beta
324 protected String createSubscriptionString(String filterSchema) {
325 StringBuilder subscriptionbuffer = new StringBuilder();
326 subscriptionbuffer.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
327 subscriptionbuffer.append(" <create-subscription\n");
328 subscriptionbuffer.append("xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">\n");
329 // FIXME Only subtree filtering supported at the moment.
330 if (filterSchema != null) {
331 subscriptionbuffer.append(" ");
332 subscriptionbuffer.append(SUBSCRIPTION_SUBTREE_FILTER_OPEN).append(NEW_LINE);
333 subscriptionbuffer.append(filterSchema).append(NEW_LINE);
334 subscriptionbuffer.append(" ");
335 subscriptionbuffer.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
336 }
337 subscriptionbuffer.append(" </create-subscription>\n");
338 subscriptionbuffer.append("</rpc>\n");
339 subscriptionbuffer.append(ENDPATTERN);
340 return subscriptionbuffer.toString();
341 }
342
343 @Override
344 public void endSubscription() throws NetconfException {
345 if (subscriptionConnected) {
346 streamHandler.setEnableNotifications(false);
347 } else {
348 throw new NetconfException("Subscription does not exist.");
349 }
350 }
351
352 private void sendHello() throws NetconfException {
353 serverHelloResponseOld = sendRequest(createHelloString(), true);
354 Matcher capabilityMatcher = CAPABILITY_REGEX_PATTERN.matcher(serverHelloResponseOld);
355 while (capabilityMatcher.find()) {
356 deviceCapabilities.add(capabilityMatcher.group(1));
357 }
358 sessionID = String.valueOf(-1);
359 Matcher sessionIDMatcher = SESSION_ID_REGEX_PATTERN.matcher(serverHelloResponseOld);
360 if (sessionIDMatcher.find()) {
361 sessionID = sessionIDMatcher.group(1);
362 } else {
363 throw new NetconfException("Missing SessionID in server hello " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200364 "reponse.");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700365 }
366
367 }
368
369 private String createHelloString() {
370 StringBuilder hellobuffer = new StringBuilder();
371 hellobuffer.append(XML_HEADER);
372 hellobuffer.append("\n");
373 hellobuffer.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
374 hellobuffer.append(" <capabilities>\n");
375 onosCapabilities.forEach(
376 cap -> hellobuffer.append(" <capability>")
377 .append(cap)
378 .append("</capability>\n"));
379 hellobuffer.append(" </capabilities>\n");
380 hellobuffer.append("</hello>\n");
381 hellobuffer.append(ENDPATTERN);
382 return hellobuffer.toString();
383
384 }
385
386 @Override
387 public void checkAndReestablish() throws NetconfException {
388 try {
389 if (client.isClosed()) {
390 log.debug("Trying to restart the whole SSH connection with {}", deviceInfo.getDeviceId());
391 cleanUp();
392 startConnection();
393 } else if (session.isClosed()) {
394 log.debug("Trying to restart the session with {}", session, deviceInfo.getDeviceId());
395 cleanUp();
396 startSession();
397 } else if (channel.isClosed()) {
398 log.debug("Trying to reopen the channel with {}", deviceInfo.getDeviceId());
399 cleanUp();
400 openChannel();
401 }
402 if (subscriptionConnected) {
403 log.debug("Restarting subscription with {}", deviceInfo.getDeviceId());
404 subscriptionConnected = false;
405 startSubscription(notificationFilterSchema);
406 }
Sean Condon7347de92017-07-21 12:17:25 +0100407 } catch (IOException | IllegalStateException e) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700408 log.error("Can't reopen connection for device {}", e.getMessage());
409 throw new NetconfException("Cannot re-open the connection with device" + deviceInfo, e);
410 }
411 }
412
413 private void cleanUp() {
414 //makes sure everything is at a clean state.
415 replies.clear();
416 }
417
418 @Override
419 public String requestSync(String request) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700420 String reply = sendRequest(request);
421 checkReply(reply);
422 return reply;
423 }
424
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200425
426 /**
427 * Validate and format netconf message.
428 *
429 * @param message to format
430 * @return formated message
431 */
432 private String formatNetconfMessage(String message) {
433 if (deviceCapabilities.contains(NETCONF_11_CAPABILITY)) {
434 message = formatChunkedMessage(message);
435 } else {
436 if (!message.contains(ENDPATTERN)) {
437 message = message + NEW_LINE + ENDPATTERN;
438 }
439 }
440 return message;
441 }
442
443 /**
444 * Validate and format message according to chunked framing mechanism.
445 *
446 * @param message to format
447 * @return formated message
448 */
449 private String formatChunkedMessage(String message) {
450 if (message.endsWith(ENDPATTERN)) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700451 // message given had Netconf 1.0 EOM pattern -> remove
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200452 message = message.substring(0, message.length() - ENDPATTERN.length());
453 }
454 if (!message.startsWith(LF + HASH)) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700455 // chunk encode message
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700456 message = LF + HASH + message.getBytes(UTF_8).length + LF + message + LF + HASH + HASH + LF;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200457 }
458 return message;
459 }
460
461
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700462 @Override
463 @Deprecated
464 public CompletableFuture<String> request(String request) {
465 return streamHandler.sendMessage(request);
466 }
467
Sean Condon54d82432017-07-26 22:27:25 +0100468 @Override
469 public int timeoutConnectSec() {
470 return connectTimeout;
471 }
472
473 @Override
474 public int timeoutReplySec() {
475 return replyTimeout;
476 }
477
478 @Override
479 public int timeoutIdleSec() {
480 return idleTimeout;
481 }
482
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700483 private CompletableFuture<String> request(String request, int messageId) {
484 return streamHandler.sendMessage(request, messageId);
485 }
486
487 private String sendRequest(String request) throws NetconfException {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700488 // FIXME probably chunk-encoding too early
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200489 request = formatNetconfMessage(request);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700490 return sendRequest(request, false);
491 }
492
493 private String sendRequest(String request, boolean isHello) throws NetconfException {
494 checkAndReestablish();
495 int messageId = -1;
496 if (!isHello) {
497 messageId = messageIdInteger.getAndIncrement();
498 }
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700499 // FIXME potentially re-writing chunked encoded String?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700500 request = formatXmlHeader(request);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200501 request = formatRequestMessageId(request, messageId);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700502 CompletableFuture<String> futureReply = request(request, messageId);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700503 String rp;
504 try {
Sean Condon54d82432017-07-26 22:27:25 +0100505 log.debug("Sending request to NETCONF with timeout {} for {}",
506 replyTimeout, deviceInfo.name());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700507 rp = futureReply.get(replyTimeout, TimeUnit.SECONDS);
Sean Condon7347de92017-07-21 12:17:25 +0100508 replies.remove(messageId); // Why here???
509 } catch (InterruptedException e) {
510 Thread.currentThread().interrupt();
511 throw new NetconfException("Interrupted waiting for reply for request" + request, e);
512 } catch (TimeoutException e) {
Sean Condon54d82432017-07-26 22:27:25 +0100513 throw new NetconfException("Timed out waiting for reply for request " +
514 request + " after " + replyTimeout + " sec.", e);
Sean Condon7347de92017-07-21 12:17:25 +0100515 } catch (ExecutionException e) {
516 log.warn("Closing session {} for {} due to unexpected Error", sessionID, deviceInfo, e);
517 try {
518 session.close();
519 channel.close(); //Closes the socket which should interrupt NetconfStreamThread
520 client.close();
521 } catch (IOException ioe) {
522 log.warn("Error closing session {} on {}", sessionID, deviceInfo, ioe);
523 }
524 NetconfDeviceOutputEvent event = new NetconfDeviceOutputEvent(
525 NetconfDeviceOutputEvent.Type.SESSION_CLOSED,
526 null, "Closed due to unexpected error " + e.getCause(),
527 Optional.of(-1), deviceInfo);
528 publishEvent(event);
529 errorReplies.clear(); // move to cleanUp()?
530 cleanUp();
531
532 throw new NetconfException("Closing session " + sessionID + " for " + deviceInfo +
533 " for request " + request, e);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700534 }
535 log.debug("Result {} from request {} to device {}", rp, request, deviceInfo);
536 return rp.trim();
537 }
538
539 private String formatRequestMessageId(String request, int messageId) {
540 if (request.contains(MESSAGE_ID_STRING)) {
541 //FIXME if application provides his own counting of messages this fails that count
542 request = request.replaceFirst(MESSAGE_ID_STRING + EQUAL + NUMBER_BETWEEN_QUOTES_MATCHER,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200543 MESSAGE_ID_STRING + EQUAL + "\"" + messageId + "\"");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700544 } else if (!request.contains(MESSAGE_ID_STRING) && !request.contains(HELLO)) {
545 //FIXME find out a better way to enforce the presence of message-id
546 request = request.replaceFirst(END_OF_RPC_OPEN_TAG, "\" " + MESSAGE_ID_STRING + EQUAL + "\""
547 + messageId + "\"" + ">");
548 }
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200549 request = updateRequestLenght(request);
550 return request;
551 }
552
553 private String updateRequestLenght(String request) {
554 if (request.contains(LF + HASH + HASH + LF)) {
555 int oldLen = Integer.parseInt(request.split(HASH)[1].split(LF)[0]);
556 String rpcWithEnding = request.substring(request.indexOf('<'));
557 String firstBlock = request.split(MSGLEN_REGEX_PATTERN)[1].split(LF + HASH + HASH + LF)[0];
558 int newLen = 0;
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700559 newLen = firstBlock.getBytes(UTF_8).length;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200560 if (oldLen != newLen) {
561 return LF + HASH + newLen + LF + rpcWithEnding;
562 }
563 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700564 return request;
565 }
566
567 private String formatXmlHeader(String request) {
568 if (!request.contains(XML_HEADER)) {
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700569 //FIXME if application provides his own XML header of different type there is a clash
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200570 if (request.startsWith(LF + HASH)) {
571 request = request.split("<")[0] + XML_HEADER + request.substring(request.split("<")[0].length());
572 } else {
573 request = XML_HEADER + "\n" + request;
574 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700575 }
576 return request;
577 }
578
579 @Override
580 public String doWrappedRpc(String request) throws NetconfException {
581 StringBuilder rpc = new StringBuilder(XML_HEADER);
582 rpc.append(RPC_OPEN);
583 rpc.append(MESSAGE_ID_STRING);
584 rpc.append(EQUAL);
585 rpc.append("\"");
586 rpc.append(messageIdInteger.get());
587 rpc.append("\" ");
588 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
589 rpc.append(request);
590 rpc.append(RPC_CLOSE).append(NEW_LINE);
591 rpc.append(ENDPATTERN);
592 String reply = sendRequest(rpc.toString());
593 checkReply(reply);
594 return reply;
595 }
596
597 @Override
598 public String get(String request) throws NetconfException {
599 return requestSync(request);
600 }
601
602 @Override
603 public String get(String filterSchema, String withDefaultsMode) throws NetconfException {
604 StringBuilder rpc = new StringBuilder(XML_HEADER);
605 rpc.append(RPC_OPEN);
606 rpc.append(MESSAGE_ID_STRING);
607 rpc.append(EQUAL);
608 rpc.append("\"");
609 rpc.append(messageIdInteger.get());
610 rpc.append("\" ");
611 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
612 rpc.append(GET_OPEN).append(NEW_LINE);
613 if (filterSchema != null) {
614 rpc.append(SUBTREE_FILTER_OPEN).append(NEW_LINE);
615 rpc.append(filterSchema).append(NEW_LINE);
616 rpc.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
617 }
618 if (withDefaultsMode != null) {
619 rpc.append(WITH_DEFAULT_OPEN).append(NETCONF_WITH_DEFAULTS_NAMESPACE).append(">");
620 rpc.append(withDefaultsMode).append(WITH_DEFAULT_CLOSE).append(NEW_LINE);
621 }
622 rpc.append(GET_CLOSE).append(NEW_LINE);
623 rpc.append(RPC_CLOSE).append(NEW_LINE);
624 rpc.append(ENDPATTERN);
625 String reply = sendRequest(rpc.toString());
626 checkReply(reply);
627 return reply;
628 }
629
630 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700631 public String getConfig(DatastoreId netconfTargetConfig) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700632 return getConfig(netconfTargetConfig, null);
633 }
634
635 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700636 public String getConfig(DatastoreId netconfTargetConfig,
637 String configurationSchema) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700638 StringBuilder rpc = new StringBuilder(XML_HEADER);
639 rpc.append("<rpc ");
640 rpc.append(MESSAGE_ID_STRING);
641 rpc.append(EQUAL);
642 rpc.append("\"");
643 rpc.append(messageIdInteger.get());
644 rpc.append("\" ");
645 rpc.append("xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
646 rpc.append("<get-config>\n");
647 rpc.append("<source>\n");
648 rpc.append("<").append(netconfTargetConfig).append("/>");
649 rpc.append("</source>");
650 if (configurationSchema != null) {
651 rpc.append("<filter type=\"subtree\">\n");
652 rpc.append(configurationSchema).append("\n");
653 rpc.append("</filter>\n");
654 }
655 rpc.append("</get-config>\n");
656 rpc.append("</rpc>\n");
657 rpc.append(ENDPATTERN);
658 String reply = sendRequest(rpc.toString());
659 return checkReply(reply) ? reply : "ERROR " + reply;
660 }
661
662 @Override
663 public boolean editConfig(String newConfiguration) throws NetconfException {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200664 if (!newConfiguration.endsWith(ENDPATTERN)) {
665 newConfiguration = newConfiguration + ENDPATTERN;
666 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700667 return checkReply(sendRequest(newConfiguration));
668 }
669
670 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700671 public boolean editConfig(DatastoreId netconfTargetConfig,
672 String mode,
673 String newConfiguration)
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700674 throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700675
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700676 newConfiguration = newConfiguration.trim();
677 StringBuilder rpc = new StringBuilder(XML_HEADER);
678 rpc.append(RPC_OPEN);
679 rpc.append(MESSAGE_ID_STRING);
680 rpc.append(EQUAL);
681 rpc.append("\"");
682 rpc.append(messageIdInteger.get());
683 rpc.append("\" ");
684 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
685 rpc.append(EDIT_CONFIG_OPEN).append("\n");
686 rpc.append(TARGET_OPEN);
687 rpc.append("<").append(netconfTargetConfig).append("/>");
688 rpc.append(TARGET_CLOSE).append("\n");
689 if (mode != null) {
690 rpc.append(DEFAULT_OPERATION_OPEN);
691 rpc.append(mode);
692 rpc.append(DEFAULT_OPERATION_CLOSE).append("\n");
693 }
694 rpc.append(CONFIG_OPEN).append("\n");
695 rpc.append(newConfiguration);
696 rpc.append(CONFIG_CLOSE).append("\n");
697 rpc.append(EDIT_CONFIG_CLOSE).append("\n");
698 rpc.append(RPC_CLOSE);
699 rpc.append(ENDPATTERN);
700 log.debug(rpc.toString());
701 String reply = sendRequest(rpc.toString());
702 return checkReply(reply);
703 }
704
705 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700706 public boolean copyConfig(DatastoreId destination,
707 DatastoreId source)
708 throws NetconfException {
709 return bareCopyConfig(destination.asXml(), source.asXml());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700710 }
711
712 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700713 public boolean copyConfig(DatastoreId netconfTargetConfig,
714 String newConfiguration)
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700715 throws NetconfException {
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700716 return bareCopyConfig(netconfTargetConfig.asXml(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200717 normalizeCopyConfigParam(newConfiguration));
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700718 }
719
720 @Override
721 public boolean copyConfig(String netconfTargetConfig,
722 String newConfiguration) throws NetconfException {
723 return bareCopyConfig(normalizeCopyConfigParam(netconfTargetConfig),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200724 normalizeCopyConfigParam(newConfiguration));
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700725 }
726
727 /**
728 * Normalize String parameter passed to copy-config API.
729 * <p>
730 * Provided for backward compatibility purpose
731 *
732 * @param input passed to copyConfig API
733 * @return XML likely to be suitable for copy-config source or target
734 */
735 private static CharSequence normalizeCopyConfigParam(String input) {
736 input = input.trim();
737 if (input.startsWith("<url")) {
738 return input;
739 } else if (!input.startsWith("<")) {
740 // assume it is a datastore name
741 return DatastoreId.datastore(input).asXml();
742 } else if (!input.startsWith("<config>")) {
743 return "<config>" + input + "</config>";
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700744 }
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700745 return input;
746 }
747
748 private boolean bareCopyConfig(CharSequence target,
749 CharSequence source)
750 throws NetconfException {
751
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700752 StringBuilder rpc = new StringBuilder(XML_HEADER);
753 rpc.append(RPC_OPEN);
754 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
755 rpc.append("<copy-config>");
756 rpc.append("<target>");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700757 rpc.append(target);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700758 rpc.append("</target>");
759 rpc.append("<source>");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700760 rpc.append(source);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700761 rpc.append("</source>");
762 rpc.append("</copy-config>");
763 rpc.append("</rpc>");
764 rpc.append(ENDPATTERN);
765 return checkReply(sendRequest(rpc.toString()));
766 }
767
768 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700769 public boolean deleteConfig(DatastoreId netconfTargetConfig) throws NetconfException {
770 if (netconfTargetConfig.equals(DatastoreId.RUNNING)) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700771 log.warn("Target configuration for delete operation can't be \"running\"",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200772 netconfTargetConfig);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700773 return false;
774 }
775 StringBuilder rpc = new StringBuilder(XML_HEADER);
776 rpc.append("<rpc>");
777 rpc.append("<delete-config>");
778 rpc.append("<target>");
779 rpc.append("<").append(netconfTargetConfig).append("/>");
780 rpc.append("</target>");
781 rpc.append("</delete-config>");
782 rpc.append("</rpc>");
783 rpc.append(ENDPATTERN);
784 return checkReply(sendRequest(rpc.toString()));
785 }
786
787 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700788 public boolean lock(DatastoreId configType) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700789 StringBuilder rpc = new StringBuilder(XML_HEADER);
790 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
791 rpc.append("<lock>");
792 rpc.append("<target>");
793 rpc.append("<");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700794 rpc.append(configType.id());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700795 rpc.append("/>");
796 rpc.append("</target>");
797 rpc.append("</lock>");
798 rpc.append("</rpc>");
799 rpc.append(ENDPATTERN);
800 String lockReply = sendRequest(rpc.toString());
801 return checkReply(lockReply);
802 }
803
804 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700805 public boolean unlock(DatastoreId configType) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700806 StringBuilder rpc = new StringBuilder(XML_HEADER);
807 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
808 rpc.append("<unlock>");
809 rpc.append("<target>");
810 rpc.append("<");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700811 rpc.append(configType.id());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700812 rpc.append("/>");
813 rpc.append("</target>");
814 rpc.append("</unlock>");
815 rpc.append("</rpc>");
816 rpc.append(ENDPATTERN);
817 String unlockReply = sendRequest(rpc.toString());
818 return checkReply(unlockReply);
819 }
820
821 @Override
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700822 public boolean close() throws NetconfException {
823 return close(false);
824 }
825
826 private boolean close(boolean force) throws NetconfException {
827 StringBuilder rpc = new StringBuilder();
828 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">");
829 if (force) {
830 rpc.append("<kill-session/>");
831 } else {
832 rpc.append("<close-session/>");
833 }
834 rpc.append("</rpc>");
835 rpc.append(ENDPATTERN);
836 return checkReply(sendRequest(rpc.toString())) || close(true);
837 }
838
839 @Override
840 public String getSessionId() {
841 return sessionID;
842 }
843
844 @Override
845 public Set<String> getDeviceCapabilitiesSet() {
846 return Collections.unmodifiableSet(deviceCapabilities);
847 }
848
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700849 @Override
850 public void setOnosCapabilities(Iterable<String> capabilities) {
851 onosCapabilities = capabilities;
852 }
853
854
855 @Override
856 public void addDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
857 streamHandler.addDeviceEventListener(listener);
858 primaryListeners.add(listener);
859 }
860
861 @Override
862 public void removeDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
863 primaryListeners.remove(listener);
864 streamHandler.removeDeviceEventListener(listener);
865 }
866
867 private boolean checkReply(String reply) throws NetconfException {
868 if (reply != null) {
869 if (!reply.contains("<rpc-error>")) {
870 log.debug("Device {} sent reply {}", deviceInfo, reply);
871 return true;
872 } else if (reply.contains("<ok/>")
873 || (reply.contains("<rpc-error>")
874 && reply.contains("warning"))) {
875 log.debug("Device {} sent reply {}", deviceInfo, reply);
876 return true;
877 }
878 }
879 log.warn("Device {} has error in reply {}", deviceInfo, reply);
880 return false;
881 }
882
Sean Condon7347de92017-07-21 12:17:25 +0100883 protected void publishEvent(NetconfDeviceOutputEvent event) {
884 primaryListeners.forEach(lsnr -> {
885 if (lsnr.isRelevant(event)) {
886 lsnr.event(event);
887 }
888 });
889 }
890
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700891 static class NotificationSession extends NetconfSessionMinaImpl {
892
893 private String notificationFilter;
894
895 NotificationSession(NetconfDeviceInfo deviceInfo)
896 throws NetconfException {
897 super(deviceInfo);
898 }
899
900 @Override
901 protected void startSubscriptionStream(String filterSchema)
902 throws NetconfException {
903
904 notificationFilter = filterSchema;
905 requestSync(createSubscriptionString(filterSchema));
906 }
907
908 @Override
909 public String toString() {
910 return MoreObjects.toStringHelper(getClass())
911 .add("deviceInfo", deviceInfo)
912 .add("sessionID", getSessionId())
913 .add("notificationFilter", notificationFilter)
914 .toString();
915 }
916 }
917
918 /**
919 * Listener attached to child session for notification streaming.
920 * <p>
921 * Forwards all notification event from child session to primary session
922 * listeners.
923 */
924 private final class NotificationForwarder
925 implements NetconfDeviceOutputEventListener {
926
927 @Override
928 public boolean isRelevant(NetconfDeviceOutputEvent event) {
929 return event.type() == Type.DEVICE_NOTIFICATION;
930 }
931
932 @Override
933 public void event(NetconfDeviceOutputEvent event) {
Sean Condon7347de92017-07-21 12:17:25 +0100934 publishEvent(event);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700935 }
936 }
937
938 public class NetconfSessionDelegateImpl implements NetconfSessionDelegate {
939
940 @Override
941 public void notify(NetconfDeviceOutputEvent event) {
942 Optional<Integer> messageId = event.getMessageID();
943 log.debug("messageID {}, waiting replies messageIDs {}", messageId,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200944 replies.keySet());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700945 if (!messageId.isPresent()) {
946 errorReplies.add(event.getMessagePayload());
947 log.error("Device {} sent error reply {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200948 event.getDeviceInfo(), event.getMessagePayload());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700949 return;
950 }
951 CompletableFuture<String> completedReply =
Sean Condon7347de92017-07-21 12:17:25 +0100952 replies.get(messageId.get()); // remove(..)?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700953 if (completedReply != null) {
954 completedReply.complete(event.getMessagePayload());
955 }
956 }
957 }
958
959 public static class MinaSshNetconfSessionFactory implements NetconfSessionFactory {
960
961 @Override
962 public NetconfSession createNetconfSession(NetconfDeviceInfo netconfDeviceInfo) throws NetconfException {
963 return new NetconfSessionMinaImpl(netconfDeviceInfo);
964 }
965 }
Sean Condon54d82432017-07-26 22:27:25 +0100966}