blob: 241ea23e8612bed5b259dd8f0a0b3c4aad55ccc9 [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) {
Holger Schulz092cbbf2017-08-31 17:52:30 +0200214 PEMParser pemParser = new PEMParser(new CharArrayReader(deviceInfo.getKey()));
215 JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700216 try {
Holger Schulz092cbbf2017-08-31 17:52:30 +0200217 KeyPair kp = converter.getKeyPair((PEMKeyPair) pemParser.readObject());
218 session.addPublicKeyIdentity(kp);
219 } catch (java.io.IOException e) {
220 throw new NetconfException("Failed to authenticate session with device " +
221 deviceInfo + "check key to be a valid key", e);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700222 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700223 } else {
224 session.addPasswordIdentity(deviceInfo.password());
225 }
Sean Condon54d82432017-07-26 22:27:25 +0100226 session.auth().verify(connectTimeout, TimeUnit.SECONDS);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700227 Set<ClientSession.ClientSessionEvent> event = session.waitFor(
228 ImmutableSet.of(ClientSession.ClientSessionEvent.WAIT_AUTH,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200229 ClientSession.ClientSessionEvent.CLOSED,
230 ClientSession.ClientSessionEvent.AUTHED), 0);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700231
232 if (!event.contains(ClientSession.ClientSessionEvent.AUTHED)) {
233 log.debug("Session closed {} {}", event, session.isClosed());
234 throw new NetconfException("Failed to authenticate session with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200235 deviceInfo + "check the user/pwd or key");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700236 }
237 openChannel();
238 }
239
240 private PublicKey getPublicKey(byte[] keyBytes, String type)
241 throws NoSuchAlgorithmException, InvalidKeySpecException {
242
243 X509EncodedKeySpec spec =
244 new X509EncodedKeySpec(keyBytes);
245 KeyFactory kf = KeyFactory.getInstance(type);
246 return kf.generatePublic(spec);
247 }
248
249 private void openChannel() throws IOException {
250 channel = session.createSubsystemChannel("netconf");
251 OpenFuture channelFuture = channel.open();
Sean Condon54d82432017-07-26 22:27:25 +0100252 if (channelFuture.await(connectTimeout, TimeUnit.SECONDS)) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700253 if (channelFuture.isOpened()) {
254 streamHandler = new NetconfStreamThread(channel.getInvertedOut(), channel.getInvertedIn(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200255 channel.getInvertedErr(), deviceInfo,
256 new NetconfSessionDelegateImpl(), replies);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700257 } else {
258 throw new NetconfException("Failed to open channel with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200259 deviceInfo);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700260 }
261 sendHello();
262 }
263 }
264
265
266 @Beta
267 protected void startSubscriptionStream(String filterSchema) throws NetconfException {
268 boolean openNewSession = false;
269 if (!deviceCapabilities.contains(INTERLEAVE_CAPABILITY_STRING)) {
270 log.info("Device {} doesn't support interleave, creating child session", deviceInfo);
271 openNewSession = true;
272
273 } else if (subscriptionConnected &&
274 notificationFilterSchema != null &&
275 !Objects.equal(filterSchema, notificationFilterSchema)) {
276 // interleave supported and existing filter is NOT "no filtering"
277 // and was requested with different filtering schema
278 log.info("Cannot use existing session for subscription {} ({})",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200279 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700280 openNewSession = true;
281 }
282
283 if (openNewSession) {
284 log.info("Creating notification session to {} with filter {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200285 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700286 NetconfSession child = new NotificationSession(deviceInfo);
287
288 child.addDeviceOutputListener(new NotificationForwarder());
289
290 child.startSubscription(filterSchema);
291 children.add(child);
292 return;
293 }
294
295 // request to start interleaved notification session
296 String reply = sendRequest(createSubscriptionString(filterSchema));
297 if (!checkReply(reply)) {
298 throw new NetconfException("Subscription not successful with device "
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200299 + deviceInfo + " with reply " + reply);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700300 }
301 subscriptionConnected = true;
302 }
303
304 @Override
305 public void startSubscription() throws NetconfException {
306 if (!subscriptionConnected) {
307 startSubscriptionStream(null);
308 }
309 streamHandler.setEnableNotifications(true);
310 }
311
312 @Beta
313 @Override
314 public void startSubscription(String filterSchema) throws NetconfException {
315 if (!subscriptionConnected) {
316 notificationFilterSchema = filterSchema;
317 startSubscriptionStream(filterSchema);
318 }
319 streamHandler.setEnableNotifications(true);
320 }
321
322 @Beta
323 protected String createSubscriptionString(String filterSchema) {
324 StringBuilder subscriptionbuffer = new StringBuilder();
325 subscriptionbuffer.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
326 subscriptionbuffer.append(" <create-subscription\n");
327 subscriptionbuffer.append("xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">\n");
328 // FIXME Only subtree filtering supported at the moment.
329 if (filterSchema != null) {
330 subscriptionbuffer.append(" ");
331 subscriptionbuffer.append(SUBSCRIPTION_SUBTREE_FILTER_OPEN).append(NEW_LINE);
332 subscriptionbuffer.append(filterSchema).append(NEW_LINE);
333 subscriptionbuffer.append(" ");
334 subscriptionbuffer.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
335 }
336 subscriptionbuffer.append(" </create-subscription>\n");
337 subscriptionbuffer.append("</rpc>\n");
338 subscriptionbuffer.append(ENDPATTERN);
339 return subscriptionbuffer.toString();
340 }
341
342 @Override
343 public void endSubscription() throws NetconfException {
344 if (subscriptionConnected) {
345 streamHandler.setEnableNotifications(false);
346 } else {
347 throw new NetconfException("Subscription does not exist.");
348 }
349 }
350
351 private void sendHello() throws NetconfException {
352 serverHelloResponseOld = sendRequest(createHelloString(), true);
353 Matcher capabilityMatcher = CAPABILITY_REGEX_PATTERN.matcher(serverHelloResponseOld);
354 while (capabilityMatcher.find()) {
355 deviceCapabilities.add(capabilityMatcher.group(1));
356 }
357 sessionID = String.valueOf(-1);
358 Matcher sessionIDMatcher = SESSION_ID_REGEX_PATTERN.matcher(serverHelloResponseOld);
359 if (sessionIDMatcher.find()) {
360 sessionID = sessionIDMatcher.group(1);
361 } else {
362 throw new NetconfException("Missing SessionID in server hello " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200363 "reponse.");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700364 }
365
366 }
367
368 private String createHelloString() {
369 StringBuilder hellobuffer = new StringBuilder();
370 hellobuffer.append(XML_HEADER);
371 hellobuffer.append("\n");
372 hellobuffer.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
373 hellobuffer.append(" <capabilities>\n");
374 onosCapabilities.forEach(
375 cap -> hellobuffer.append(" <capability>")
376 .append(cap)
377 .append("</capability>\n"));
378 hellobuffer.append(" </capabilities>\n");
379 hellobuffer.append("</hello>\n");
380 hellobuffer.append(ENDPATTERN);
381 return hellobuffer.toString();
382
383 }
384
385 @Override
386 public void checkAndReestablish() throws NetconfException {
387 try {
388 if (client.isClosed()) {
389 log.debug("Trying to restart the whole SSH connection with {}", deviceInfo.getDeviceId());
390 cleanUp();
391 startConnection();
392 } else if (session.isClosed()) {
393 log.debug("Trying to restart the session with {}", session, deviceInfo.getDeviceId());
394 cleanUp();
395 startSession();
396 } else if (channel.isClosed()) {
397 log.debug("Trying to reopen the channel with {}", deviceInfo.getDeviceId());
398 cleanUp();
399 openChannel();
400 }
401 if (subscriptionConnected) {
402 log.debug("Restarting subscription with {}", deviceInfo.getDeviceId());
403 subscriptionConnected = false;
404 startSubscription(notificationFilterSchema);
405 }
Sean Condon7347de92017-07-21 12:17:25 +0100406 } catch (IOException | IllegalStateException e) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700407 log.error("Can't reopen connection for device {}", e.getMessage());
408 throw new NetconfException("Cannot re-open the connection with device" + deviceInfo, e);
409 }
410 }
411
412 private void cleanUp() {
413 //makes sure everything is at a clean state.
414 replies.clear();
415 }
416
417 @Override
418 public String requestSync(String request) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700419 String reply = sendRequest(request);
420 checkReply(reply);
421 return reply;
422 }
423
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200424
425 /**
426 * Validate and format netconf message.
427 *
428 * @param message to format
429 * @return formated message
430 */
431 private String formatNetconfMessage(String message) {
432 if (deviceCapabilities.contains(NETCONF_11_CAPABILITY)) {
433 message = formatChunkedMessage(message);
434 } else {
435 if (!message.contains(ENDPATTERN)) {
436 message = message + NEW_LINE + ENDPATTERN;
437 }
438 }
439 return message;
440 }
441
442 /**
443 * Validate and format message according to chunked framing mechanism.
444 *
445 * @param message to format
446 * @return formated message
447 */
448 private String formatChunkedMessage(String message) {
449 if (message.endsWith(ENDPATTERN)) {
450 message = message.substring(0, message.length() - ENDPATTERN.length());
451 }
452 if (!message.startsWith(LF + HASH)) {
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700453 message = LF + HASH + message.getBytes(UTF_8).length + LF + message + LF + HASH + HASH + LF;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200454 }
455 return message;
456 }
457
458
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700459 @Override
460 @Deprecated
461 public CompletableFuture<String> request(String request) {
462 return streamHandler.sendMessage(request);
463 }
464
Sean Condon54d82432017-07-26 22:27:25 +0100465 @Override
466 public int timeoutConnectSec() {
467 return connectTimeout;
468 }
469
470 @Override
471 public int timeoutReplySec() {
472 return replyTimeout;
473 }
474
475 @Override
476 public int timeoutIdleSec() {
477 return idleTimeout;
478 }
479
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700480 private CompletableFuture<String> request(String request, int messageId) {
481 return streamHandler.sendMessage(request, messageId);
482 }
483
484 private String sendRequest(String request) throws NetconfException {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200485 request = formatNetconfMessage(request);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700486 return sendRequest(request, false);
487 }
488
489 private String sendRequest(String request, boolean isHello) throws NetconfException {
490 checkAndReestablish();
491 int messageId = -1;
492 if (!isHello) {
493 messageId = messageIdInteger.getAndIncrement();
494 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700495 request = formatXmlHeader(request);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200496 request = formatRequestMessageId(request, messageId);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700497 CompletableFuture<String> futureReply = request(request, messageId);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700498 String rp;
499 try {
Sean Condon54d82432017-07-26 22:27:25 +0100500 log.debug("Sending request to NETCONF with timeout {} for {}",
501 replyTimeout, deviceInfo.name());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700502 rp = futureReply.get(replyTimeout, TimeUnit.SECONDS);
Sean Condon7347de92017-07-21 12:17:25 +0100503 replies.remove(messageId); // Why here???
504 } catch (InterruptedException e) {
505 Thread.currentThread().interrupt();
506 throw new NetconfException("Interrupted waiting for reply for request" + request, e);
507 } catch (TimeoutException e) {
Sean Condon54d82432017-07-26 22:27:25 +0100508 throw new NetconfException("Timed out waiting for reply for request " +
509 request + " after " + replyTimeout + " sec.", e);
Sean Condon7347de92017-07-21 12:17:25 +0100510 } catch (ExecutionException e) {
511 log.warn("Closing session {} for {} due to unexpected Error", sessionID, deviceInfo, e);
512 try {
513 session.close();
514 channel.close(); //Closes the socket which should interrupt NetconfStreamThread
515 client.close();
516 } catch (IOException ioe) {
517 log.warn("Error closing session {} on {}", sessionID, deviceInfo, ioe);
518 }
519 NetconfDeviceOutputEvent event = new NetconfDeviceOutputEvent(
520 NetconfDeviceOutputEvent.Type.SESSION_CLOSED,
521 null, "Closed due to unexpected error " + e.getCause(),
522 Optional.of(-1), deviceInfo);
523 publishEvent(event);
524 errorReplies.clear(); // move to cleanUp()?
525 cleanUp();
526
527 throw new NetconfException("Closing session " + sessionID + " for " + deviceInfo +
528 " for request " + request, e);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700529 }
530 log.debug("Result {} from request {} to device {}", rp, request, deviceInfo);
531 return rp.trim();
532 }
533
534 private String formatRequestMessageId(String request, int messageId) {
535 if (request.contains(MESSAGE_ID_STRING)) {
536 //FIXME if application provides his own counting of messages this fails that count
537 request = request.replaceFirst(MESSAGE_ID_STRING + EQUAL + NUMBER_BETWEEN_QUOTES_MATCHER,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200538 MESSAGE_ID_STRING + EQUAL + "\"" + messageId + "\"");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700539 } else if (!request.contains(MESSAGE_ID_STRING) && !request.contains(HELLO)) {
540 //FIXME find out a better way to enforce the presence of message-id
541 request = request.replaceFirst(END_OF_RPC_OPEN_TAG, "\" " + MESSAGE_ID_STRING + EQUAL + "\""
542 + messageId + "\"" + ">");
543 }
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200544 request = updateRequestLenght(request);
545 return request;
546 }
547
548 private String updateRequestLenght(String request) {
549 if (request.contains(LF + HASH + HASH + LF)) {
550 int oldLen = Integer.parseInt(request.split(HASH)[1].split(LF)[0]);
551 String rpcWithEnding = request.substring(request.indexOf('<'));
552 String firstBlock = request.split(MSGLEN_REGEX_PATTERN)[1].split(LF + HASH + HASH + LF)[0];
553 int newLen = 0;
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700554 newLen = firstBlock.getBytes(UTF_8).length;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200555 if (oldLen != newLen) {
556 return LF + HASH + newLen + LF + rpcWithEnding;
557 }
558 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700559 return request;
560 }
561
562 private String formatXmlHeader(String request) {
563 if (!request.contains(XML_HEADER)) {
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700564 //FIXME if application provides his own XML header of different type there is a clash
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200565 if (request.startsWith(LF + HASH)) {
566 request = request.split("<")[0] + XML_HEADER + request.substring(request.split("<")[0].length());
567 } else {
568 request = XML_HEADER + "\n" + request;
569 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700570 }
571 return request;
572 }
573
574 @Override
575 public String doWrappedRpc(String request) throws NetconfException {
576 StringBuilder rpc = new StringBuilder(XML_HEADER);
577 rpc.append(RPC_OPEN);
578 rpc.append(MESSAGE_ID_STRING);
579 rpc.append(EQUAL);
580 rpc.append("\"");
581 rpc.append(messageIdInteger.get());
582 rpc.append("\" ");
583 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
584 rpc.append(request);
585 rpc.append(RPC_CLOSE).append(NEW_LINE);
586 rpc.append(ENDPATTERN);
587 String reply = sendRequest(rpc.toString());
588 checkReply(reply);
589 return reply;
590 }
591
592 @Override
593 public String get(String request) throws NetconfException {
594 return requestSync(request);
595 }
596
597 @Override
598 public String get(String filterSchema, String withDefaultsMode) throws NetconfException {
599 StringBuilder rpc = new StringBuilder(XML_HEADER);
600 rpc.append(RPC_OPEN);
601 rpc.append(MESSAGE_ID_STRING);
602 rpc.append(EQUAL);
603 rpc.append("\"");
604 rpc.append(messageIdInteger.get());
605 rpc.append("\" ");
606 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
607 rpc.append(GET_OPEN).append(NEW_LINE);
608 if (filterSchema != null) {
609 rpc.append(SUBTREE_FILTER_OPEN).append(NEW_LINE);
610 rpc.append(filterSchema).append(NEW_LINE);
611 rpc.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
612 }
613 if (withDefaultsMode != null) {
614 rpc.append(WITH_DEFAULT_OPEN).append(NETCONF_WITH_DEFAULTS_NAMESPACE).append(">");
615 rpc.append(withDefaultsMode).append(WITH_DEFAULT_CLOSE).append(NEW_LINE);
616 }
617 rpc.append(GET_CLOSE).append(NEW_LINE);
618 rpc.append(RPC_CLOSE).append(NEW_LINE);
619 rpc.append(ENDPATTERN);
620 String reply = sendRequest(rpc.toString());
621 checkReply(reply);
622 return reply;
623 }
624
625 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700626 public String getConfig(DatastoreId netconfTargetConfig) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700627 return getConfig(netconfTargetConfig, null);
628 }
629
630 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700631 public String getConfig(DatastoreId netconfTargetConfig,
632 String configurationSchema) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700633 StringBuilder rpc = new StringBuilder(XML_HEADER);
634 rpc.append("<rpc ");
635 rpc.append(MESSAGE_ID_STRING);
636 rpc.append(EQUAL);
637 rpc.append("\"");
638 rpc.append(messageIdInteger.get());
639 rpc.append("\" ");
640 rpc.append("xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
641 rpc.append("<get-config>\n");
642 rpc.append("<source>\n");
643 rpc.append("<").append(netconfTargetConfig).append("/>");
644 rpc.append("</source>");
645 if (configurationSchema != null) {
646 rpc.append("<filter type=\"subtree\">\n");
647 rpc.append(configurationSchema).append("\n");
648 rpc.append("</filter>\n");
649 }
650 rpc.append("</get-config>\n");
651 rpc.append("</rpc>\n");
652 rpc.append(ENDPATTERN);
653 String reply = sendRequest(rpc.toString());
654 return checkReply(reply) ? reply : "ERROR " + reply;
655 }
656
657 @Override
658 public boolean editConfig(String newConfiguration) throws NetconfException {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200659 if (!newConfiguration.endsWith(ENDPATTERN)) {
660 newConfiguration = newConfiguration + ENDPATTERN;
661 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700662 return checkReply(sendRequest(newConfiguration));
663 }
664
665 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700666 public boolean editConfig(DatastoreId netconfTargetConfig,
667 String mode,
668 String newConfiguration)
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700669 throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700670
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700671 newConfiguration = newConfiguration.trim();
672 StringBuilder rpc = new StringBuilder(XML_HEADER);
673 rpc.append(RPC_OPEN);
674 rpc.append(MESSAGE_ID_STRING);
675 rpc.append(EQUAL);
676 rpc.append("\"");
677 rpc.append(messageIdInteger.get());
678 rpc.append("\" ");
679 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
680 rpc.append(EDIT_CONFIG_OPEN).append("\n");
681 rpc.append(TARGET_OPEN);
682 rpc.append("<").append(netconfTargetConfig).append("/>");
683 rpc.append(TARGET_CLOSE).append("\n");
684 if (mode != null) {
685 rpc.append(DEFAULT_OPERATION_OPEN);
686 rpc.append(mode);
687 rpc.append(DEFAULT_OPERATION_CLOSE).append("\n");
688 }
689 rpc.append(CONFIG_OPEN).append("\n");
690 rpc.append(newConfiguration);
691 rpc.append(CONFIG_CLOSE).append("\n");
692 rpc.append(EDIT_CONFIG_CLOSE).append("\n");
693 rpc.append(RPC_CLOSE);
694 rpc.append(ENDPATTERN);
695 log.debug(rpc.toString());
696 String reply = sendRequest(rpc.toString());
697 return checkReply(reply);
698 }
699
700 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700701 public boolean copyConfig(DatastoreId destination,
702 DatastoreId source)
703 throws NetconfException {
704 return bareCopyConfig(destination.asXml(), source.asXml());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700705 }
706
707 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700708 public boolean copyConfig(DatastoreId netconfTargetConfig,
709 String newConfiguration)
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700710 throws NetconfException {
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700711 return bareCopyConfig(netconfTargetConfig.asXml(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200712 normalizeCopyConfigParam(newConfiguration));
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700713 }
714
715 @Override
716 public boolean copyConfig(String netconfTargetConfig,
717 String newConfiguration) throws NetconfException {
718 return bareCopyConfig(normalizeCopyConfigParam(netconfTargetConfig),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200719 normalizeCopyConfigParam(newConfiguration));
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700720 }
721
722 /**
723 * Normalize String parameter passed to copy-config API.
724 * <p>
725 * Provided for backward compatibility purpose
726 *
727 * @param input passed to copyConfig API
728 * @return XML likely to be suitable for copy-config source or target
729 */
730 private static CharSequence normalizeCopyConfigParam(String input) {
731 input = input.trim();
732 if (input.startsWith("<url")) {
733 return input;
734 } else if (!input.startsWith("<")) {
735 // assume it is a datastore name
736 return DatastoreId.datastore(input).asXml();
737 } else if (!input.startsWith("<config>")) {
738 return "<config>" + input + "</config>";
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700739 }
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700740 return input;
741 }
742
743 private boolean bareCopyConfig(CharSequence target,
744 CharSequence source)
745 throws NetconfException {
746
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700747 StringBuilder rpc = new StringBuilder(XML_HEADER);
748 rpc.append(RPC_OPEN);
749 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
750 rpc.append("<copy-config>");
751 rpc.append("<target>");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700752 rpc.append(target);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700753 rpc.append("</target>");
754 rpc.append("<source>");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700755 rpc.append(source);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700756 rpc.append("</source>");
757 rpc.append("</copy-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 deleteConfig(DatastoreId netconfTargetConfig) throws NetconfException {
765 if (netconfTargetConfig.equals(DatastoreId.RUNNING)) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700766 log.warn("Target configuration for delete operation can't be \"running\"",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200767 netconfTargetConfig);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700768 return false;
769 }
770 StringBuilder rpc = new StringBuilder(XML_HEADER);
771 rpc.append("<rpc>");
772 rpc.append("<delete-config>");
773 rpc.append("<target>");
774 rpc.append("<").append(netconfTargetConfig).append("/>");
775 rpc.append("</target>");
776 rpc.append("</delete-config>");
777 rpc.append("</rpc>");
778 rpc.append(ENDPATTERN);
779 return checkReply(sendRequest(rpc.toString()));
780 }
781
782 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700783 public boolean lock(DatastoreId configType) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700784 StringBuilder rpc = new StringBuilder(XML_HEADER);
785 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
786 rpc.append("<lock>");
787 rpc.append("<target>");
788 rpc.append("<");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700789 rpc.append(configType.id());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700790 rpc.append("/>");
791 rpc.append("</target>");
792 rpc.append("</lock>");
793 rpc.append("</rpc>");
794 rpc.append(ENDPATTERN);
795 String lockReply = sendRequest(rpc.toString());
796 return checkReply(lockReply);
797 }
798
799 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700800 public boolean unlock(DatastoreId configType) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700801 StringBuilder rpc = new StringBuilder(XML_HEADER);
802 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
803 rpc.append("<unlock>");
804 rpc.append("<target>");
805 rpc.append("<");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700806 rpc.append(configType.id());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700807 rpc.append("/>");
808 rpc.append("</target>");
809 rpc.append("</unlock>");
810 rpc.append("</rpc>");
811 rpc.append(ENDPATTERN);
812 String unlockReply = sendRequest(rpc.toString());
813 return checkReply(unlockReply);
814 }
815
816 @Override
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700817 public boolean close() throws NetconfException {
818 return close(false);
819 }
820
821 private boolean close(boolean force) throws NetconfException {
822 StringBuilder rpc = new StringBuilder();
823 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">");
824 if (force) {
825 rpc.append("<kill-session/>");
826 } else {
827 rpc.append("<close-session/>");
828 }
829 rpc.append("</rpc>");
830 rpc.append(ENDPATTERN);
831 return checkReply(sendRequest(rpc.toString())) || close(true);
832 }
833
834 @Override
835 public String getSessionId() {
836 return sessionID;
837 }
838
839 @Override
840 public Set<String> getDeviceCapabilitiesSet() {
841 return Collections.unmodifiableSet(deviceCapabilities);
842 }
843
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700844 @Override
845 public void setOnosCapabilities(Iterable<String> capabilities) {
846 onosCapabilities = capabilities;
847 }
848
849
850 @Override
851 public void addDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
852 streamHandler.addDeviceEventListener(listener);
853 primaryListeners.add(listener);
854 }
855
856 @Override
857 public void removeDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
858 primaryListeners.remove(listener);
859 streamHandler.removeDeviceEventListener(listener);
860 }
861
862 private boolean checkReply(String reply) throws NetconfException {
863 if (reply != null) {
864 if (!reply.contains("<rpc-error>")) {
865 log.debug("Device {} sent reply {}", deviceInfo, reply);
866 return true;
867 } else if (reply.contains("<ok/>")
868 || (reply.contains("<rpc-error>")
869 && reply.contains("warning"))) {
870 log.debug("Device {} sent reply {}", deviceInfo, reply);
871 return true;
872 }
873 }
874 log.warn("Device {} has error in reply {}", deviceInfo, reply);
875 return false;
876 }
877
Sean Condon7347de92017-07-21 12:17:25 +0100878 protected void publishEvent(NetconfDeviceOutputEvent event) {
879 primaryListeners.forEach(lsnr -> {
880 if (lsnr.isRelevant(event)) {
881 lsnr.event(event);
882 }
883 });
884 }
885
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700886 static class NotificationSession extends NetconfSessionMinaImpl {
887
888 private String notificationFilter;
889
890 NotificationSession(NetconfDeviceInfo deviceInfo)
891 throws NetconfException {
892 super(deviceInfo);
893 }
894
895 @Override
896 protected void startSubscriptionStream(String filterSchema)
897 throws NetconfException {
898
899 notificationFilter = filterSchema;
900 requestSync(createSubscriptionString(filterSchema));
901 }
902
903 @Override
904 public String toString() {
905 return MoreObjects.toStringHelper(getClass())
906 .add("deviceInfo", deviceInfo)
907 .add("sessionID", getSessionId())
908 .add("notificationFilter", notificationFilter)
909 .toString();
910 }
911 }
912
913 /**
914 * Listener attached to child session for notification streaming.
915 * <p>
916 * Forwards all notification event from child session to primary session
917 * listeners.
918 */
919 private final class NotificationForwarder
920 implements NetconfDeviceOutputEventListener {
921
922 @Override
923 public boolean isRelevant(NetconfDeviceOutputEvent event) {
924 return event.type() == Type.DEVICE_NOTIFICATION;
925 }
926
927 @Override
928 public void event(NetconfDeviceOutputEvent event) {
Sean Condon7347de92017-07-21 12:17:25 +0100929 publishEvent(event);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700930 }
931 }
932
933 public class NetconfSessionDelegateImpl implements NetconfSessionDelegate {
934
935 @Override
936 public void notify(NetconfDeviceOutputEvent event) {
937 Optional<Integer> messageId = event.getMessageID();
938 log.debug("messageID {}, waiting replies messageIDs {}", messageId,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200939 replies.keySet());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700940 if (!messageId.isPresent()) {
941 errorReplies.add(event.getMessagePayload());
942 log.error("Device {} sent error reply {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200943 event.getDeviceInfo(), event.getMessagePayload());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700944 return;
945 }
946 CompletableFuture<String> completedReply =
Sean Condon7347de92017-07-21 12:17:25 +0100947 replies.get(messageId.get()); // remove(..)?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700948 if (completedReply != null) {
949 completedReply.complete(event.getMessagePayload());
950 }
951 }
952 }
953
954 public static class MinaSshNetconfSessionFactory implements NetconfSessionFactory {
955
956 @Override
957 public NetconfSession createNetconfSession(NetconfDeviceInfo netconfDeviceInfo) throws NetconfException {
958 return new NetconfSessionMinaImpl(netconfDeviceInfo);
959 }
960 }
Sean Condon54d82432017-07-26 22:27:25 +0100961}