blob: 693866189bb27aed21c5fcdb3bbaacd61a063ffd [file] [log] [blame]
Andrea Campanella7bbe7b12017-05-03 16:03:38 -07001/*
2 * Copyright 2015-present Open Networking Laboratory
3 *
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;
22import com.google.common.collect.ImmutableSet;
23import org.apache.sshd.client.SshClient;
24import org.apache.sshd.client.channel.ClientChannel;
25import org.apache.sshd.client.future.ConnectFuture;
26import org.apache.sshd.client.future.OpenFuture;
27import org.apache.sshd.client.session.ClientSession;
Sean Condon7347de92017-07-21 12:17:25 +010028import org.apache.sshd.common.FactoryManager;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070029import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -070030import org.onosproject.netconf.DatastoreId;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070031import org.onosproject.netconf.NetconfDeviceInfo;
32import org.onosproject.netconf.NetconfDeviceOutputEvent;
33import org.onosproject.netconf.NetconfDeviceOutputEvent.Type;
34import org.onosproject.netconf.NetconfDeviceOutputEventListener;
35import org.onosproject.netconf.NetconfException;
36import org.onosproject.netconf.NetconfSession;
37import org.onosproject.netconf.NetconfSessionFactory;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070038import org.slf4j.Logger;
39import org.slf4j.LoggerFactory;
40import java.io.IOException;
41import java.nio.ByteBuffer;
42import java.nio.CharBuffer;
43import java.nio.charset.StandardCharsets;
44import java.security.KeyFactory;
45import java.security.KeyPair;
46import java.security.NoSuchAlgorithmException;
47import java.security.PublicKey;
48import java.security.spec.InvalidKeySpecException;
49import java.security.spec.X509EncodedKeySpec;
50import java.util.ArrayList;
51import java.util.Collection;
52import java.util.Collections;
53import java.util.LinkedHashSet;
54import java.util.List;
55import java.util.Map;
56import java.util.Optional;
57import java.util.Set;
58import java.util.concurrent.CompletableFuture;
59import java.util.concurrent.ConcurrentHashMap;
60import java.util.concurrent.CopyOnWriteArrayList;
61import java.util.concurrent.ExecutionException;
62import java.util.concurrent.TimeUnit;
63import java.util.concurrent.TimeoutException;
64import java.util.concurrent.atomic.AtomicInteger;
65import java.util.regex.Matcher;
66import java.util.regex.Pattern;
67
68/**
69 * Implementation of a NETCONF session to talk to a device.
70 */
71public class NetconfSessionMinaImpl implements NetconfSession {
72
73 private static final Logger log = LoggerFactory
74 .getLogger(NetconfSessionMinaImpl.class);
75
76 private static final String ENDPATTERN = "]]>]]>";
77 private static final String MESSAGE_ID_STRING = "message-id";
78 private static final String HELLO = "<hello";
79 private static final String NEW_LINE = "\n";
80 private static final String END_OF_RPC_OPEN_TAG = "\">";
81 private static final String EQUAL = "=";
82 private static final String NUMBER_BETWEEN_QUOTES_MATCHER = "\"+([0-9]+)+\"";
83 private static final String RPC_OPEN = "<rpc ";
84 private static final String RPC_CLOSE = "</rpc>";
85 private static final String GET_OPEN = "<get>";
86 private static final String GET_CLOSE = "</get>";
87 private static final String WITH_DEFAULT_OPEN = "<with-defaults ";
88 private static final String WITH_DEFAULT_CLOSE = "</with-defaults>";
89 private static final String DEFAULT_OPERATION_OPEN = "<default-operation>";
90 private static final String DEFAULT_OPERATION_CLOSE = "</default-operation>";
91 private static final String SUBTREE_FILTER_OPEN = "<filter type=\"subtree\">";
92 private static final String SUBTREE_FILTER_CLOSE = "</filter>";
93 private static final String EDIT_CONFIG_OPEN = "<edit-config>";
94 private static final String EDIT_CONFIG_CLOSE = "</edit-config>";
95 private static final String TARGET_OPEN = "<target>";
96 private static final String TARGET_CLOSE = "</target>";
97 private static final String CONFIG_OPEN = "<config xmlns:nc=\"urn:ietf:params:xml:ns:netconf:base:1.0\">";
98 private static final String CONFIG_CLOSE = "</config>";
99 private static final String XML_HEADER =
100 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
101 private static final String NETCONF_BASE_NAMESPACE =
102 "xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\"";
103 private static final String NETCONF_WITH_DEFAULTS_NAMESPACE =
104 "xmlns=\"urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults\"";
105 private static final String SUBSCRIPTION_SUBTREE_FILTER_OPEN =
106 "<filter xmlns:base10=\"urn:ietf:params:xml:ns:netconf:base:1.0\" base10:type=\"subtree\">";
107
108 private static final String INTERLEAVE_CAPABILITY_STRING = "urn:ietf:params:netconf:capability:interleave:1.0";
109
110 private static final String CAPABILITY_REGEX = "<capability>\\s*(.*?)\\s*</capability>";
111 private static final Pattern CAPABILITY_REGEX_PATTERN = Pattern.compile(CAPABILITY_REGEX);
112
113 private static final String SESSION_ID_REGEX = "<session-id>\\s*(.*?)\\s*</session-id>";
114 private static final Pattern SESSION_ID_REGEX_PATTERN = Pattern.compile(SESSION_ID_REGEX);
115 private static final String RSA = "RSA";
116 private static final String DSA = "DSA";
117
118 private String sessionID;
119 private final AtomicInteger messageIdInteger = new AtomicInteger(1);
120 protected final NetconfDeviceInfo deviceInfo;
121 private Iterable<String> onosCapabilities =
122 Collections.singletonList("urn:ietf:params:netconf:base:1.0");
123
124 /* NOTE: the "serverHelloResponseOld" is deprecated in 1.10.0 and should eventually be removed */
125 @Deprecated
126 private String serverHelloResponseOld;
127 private final Set<String> deviceCapabilities = new LinkedHashSet<>();
128 private NetconfStreamHandler streamHandler;
129 private Map<Integer, CompletableFuture<String>> replies;
Sean Condon7347de92017-07-21 12:17:25 +0100130 private List<String> errorReplies; // Not sure why we need this?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700131 private boolean subscriptionConnected = false;
132 private String notificationFilterSchema = null;
133
134 private final Collection<NetconfDeviceOutputEventListener> primaryListeners =
135 new CopyOnWriteArrayList<>();
136 private final Collection<NetconfSession> children =
137 new CopyOnWriteArrayList<>();
138
139
140 private ClientChannel channel = null;
141 private ClientSession session = null;
142 private SshClient client = null;
143
144
145 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo) throws NetconfException {
146 this.deviceInfo = deviceInfo;
147 replies = new ConcurrentHashMap<>();
148 errorReplies = new ArrayList<>();
149 startConnection();
150 }
151
152 private void startConnection() throws NetconfException {
153 try {
154 startClient();
155 } catch (IOException e) {
156 throw new NetconfException("Failed to establish SSH with device " + deviceInfo, e);
157 }
158 }
159
160 private void startClient() throws IOException {
161 client = SshClient.setUpDefaultClient();
Sean Condon7347de92017-07-21 12:17:25 +0100162 int replyTimeoutSec = NetconfControllerImpl.netconfIdleTimeout;
163 client.getProperties().putIfAbsent(FactoryManager.IDLE_TIMEOUT,
164 TimeUnit.SECONDS.toMillis(replyTimeoutSec));
165 client.getProperties().putIfAbsent(FactoryManager.NIO2_READ_TIMEOUT,
166 TimeUnit.SECONDS.toMillis(replyTimeoutSec + 15L));
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700167 client.start();
168 client.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
169 startSession();
170 }
171
172 private void startSession() throws IOException {
173 final ConnectFuture connectFuture;
174 connectFuture = client.connect(deviceInfo.name(),
175 deviceInfo.ip().toString(),
176 deviceInfo.port())
177 .verify(NetconfControllerImpl.netconfConnectTimeout, TimeUnit.SECONDS);
178 session = connectFuture.getSession();
179 //Using the device ssh key if possible
180 if (deviceInfo.getKey() != null) {
181 ByteBuffer buf = StandardCharsets.UTF_8.encode(CharBuffer.wrap(deviceInfo.getKey()));
182 byte[] byteKey = new byte[buf.limit()];
183 buf.get(byteKey);
184 PublicKey key;
185 try {
186 key = getPublicKey(byteKey, RSA);
187 } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
188 try {
189 key = getPublicKey(byteKey, DSA);
190 } catch (NoSuchAlgorithmException | InvalidKeySpecException e1) {
191 throw new NetconfException("Failed to authenticate session with device " +
192 deviceInfo + "check key to be the " +
193 "proper DSA or RSA key", e1);
194 }
195 }
196 //privateKye can set tu null because is not used by the method.
197 session.addPublicKeyIdentity(new KeyPair(key, null));
198 } else {
199 session.addPasswordIdentity(deviceInfo.password());
200 }
201 session.auth().verify(NetconfControllerImpl.netconfConnectTimeout, TimeUnit.SECONDS);
202 Set<ClientSession.ClientSessionEvent> event = session.waitFor(
203 ImmutableSet.of(ClientSession.ClientSessionEvent.WAIT_AUTH,
204 ClientSession.ClientSessionEvent.CLOSED,
205 ClientSession.ClientSessionEvent.AUTHED), 0);
206
207 if (!event.contains(ClientSession.ClientSessionEvent.AUTHED)) {
208 log.debug("Session closed {} {}", event, session.isClosed());
209 throw new NetconfException("Failed to authenticate session with device " +
210 deviceInfo + "check the user/pwd or key");
211 }
212 openChannel();
213 }
214
215 private PublicKey getPublicKey(byte[] keyBytes, String type)
216 throws NoSuchAlgorithmException, InvalidKeySpecException {
217
218 X509EncodedKeySpec spec =
219 new X509EncodedKeySpec(keyBytes);
220 KeyFactory kf = KeyFactory.getInstance(type);
221 return kf.generatePublic(spec);
222 }
223
224 private void openChannel() throws IOException {
225 channel = session.createSubsystemChannel("netconf");
226 OpenFuture channelFuture = channel.open();
227 if (channelFuture.await(NetconfControllerImpl.netconfConnectTimeout, TimeUnit.SECONDS)) {
228 if (channelFuture.isOpened()) {
229 streamHandler = new NetconfStreamThread(channel.getInvertedOut(), channel.getInvertedIn(),
230 channel.getInvertedErr(), deviceInfo,
231 new NetconfSessionDelegateImpl(), replies);
232 } else {
233 throw new NetconfException("Failed to open channel with device " +
234 deviceInfo);
235 }
236 sendHello();
237 }
238 }
239
240
241 @Beta
242 protected void startSubscriptionStream(String filterSchema) throws NetconfException {
243 boolean openNewSession = false;
244 if (!deviceCapabilities.contains(INTERLEAVE_CAPABILITY_STRING)) {
245 log.info("Device {} doesn't support interleave, creating child session", deviceInfo);
246 openNewSession = true;
247
248 } else if (subscriptionConnected &&
249 notificationFilterSchema != null &&
250 !Objects.equal(filterSchema, notificationFilterSchema)) {
251 // interleave supported and existing filter is NOT "no filtering"
252 // and was requested with different filtering schema
253 log.info("Cannot use existing session for subscription {} ({})",
254 deviceInfo, filterSchema);
255 openNewSession = true;
256 }
257
258 if (openNewSession) {
259 log.info("Creating notification session to {} with filter {}",
260 deviceInfo, filterSchema);
261 NetconfSession child = new NotificationSession(deviceInfo);
262
263 child.addDeviceOutputListener(new NotificationForwarder());
264
265 child.startSubscription(filterSchema);
266 children.add(child);
267 return;
268 }
269
270 // request to start interleaved notification session
271 String reply = sendRequest(createSubscriptionString(filterSchema));
272 if (!checkReply(reply)) {
273 throw new NetconfException("Subscription not successful with device "
274 + deviceInfo + " with reply " + reply);
275 }
276 subscriptionConnected = true;
277 }
278
279 @Override
280 public void startSubscription() throws NetconfException {
281 if (!subscriptionConnected) {
282 startSubscriptionStream(null);
283 }
284 streamHandler.setEnableNotifications(true);
285 }
286
287 @Beta
288 @Override
289 public void startSubscription(String filterSchema) throws NetconfException {
290 if (!subscriptionConnected) {
291 notificationFilterSchema = filterSchema;
292 startSubscriptionStream(filterSchema);
293 }
294 streamHandler.setEnableNotifications(true);
295 }
296
297 @Beta
298 protected String createSubscriptionString(String filterSchema) {
299 StringBuilder subscriptionbuffer = new StringBuilder();
300 subscriptionbuffer.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
301 subscriptionbuffer.append(" <create-subscription\n");
302 subscriptionbuffer.append("xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">\n");
303 // FIXME Only subtree filtering supported at the moment.
304 if (filterSchema != null) {
305 subscriptionbuffer.append(" ");
306 subscriptionbuffer.append(SUBSCRIPTION_SUBTREE_FILTER_OPEN).append(NEW_LINE);
307 subscriptionbuffer.append(filterSchema).append(NEW_LINE);
308 subscriptionbuffer.append(" ");
309 subscriptionbuffer.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
310 }
311 subscriptionbuffer.append(" </create-subscription>\n");
312 subscriptionbuffer.append("</rpc>\n");
313 subscriptionbuffer.append(ENDPATTERN);
314 return subscriptionbuffer.toString();
315 }
316
317 @Override
318 public void endSubscription() throws NetconfException {
319 if (subscriptionConnected) {
320 streamHandler.setEnableNotifications(false);
321 } else {
322 throw new NetconfException("Subscription does not exist.");
323 }
324 }
325
326 private void sendHello() throws NetconfException {
327 serverHelloResponseOld = sendRequest(createHelloString(), true);
328 Matcher capabilityMatcher = CAPABILITY_REGEX_PATTERN.matcher(serverHelloResponseOld);
329 while (capabilityMatcher.find()) {
330 deviceCapabilities.add(capabilityMatcher.group(1));
331 }
332 sessionID = String.valueOf(-1);
333 Matcher sessionIDMatcher = SESSION_ID_REGEX_PATTERN.matcher(serverHelloResponseOld);
334 if (sessionIDMatcher.find()) {
335 sessionID = sessionIDMatcher.group(1);
336 } else {
337 throw new NetconfException("Missing SessionID in server hello " +
338 "reponse.");
339 }
340
341 }
342
343 private String createHelloString() {
344 StringBuilder hellobuffer = new StringBuilder();
345 hellobuffer.append(XML_HEADER);
346 hellobuffer.append("\n");
347 hellobuffer.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
348 hellobuffer.append(" <capabilities>\n");
349 onosCapabilities.forEach(
350 cap -> hellobuffer.append(" <capability>")
351 .append(cap)
352 .append("</capability>\n"));
353 hellobuffer.append(" </capabilities>\n");
354 hellobuffer.append("</hello>\n");
355 hellobuffer.append(ENDPATTERN);
356 return hellobuffer.toString();
357
358 }
359
360 @Override
361 public void checkAndReestablish() throws NetconfException {
362 try {
363 if (client.isClosed()) {
364 log.debug("Trying to restart the whole SSH connection with {}", deviceInfo.getDeviceId());
365 cleanUp();
366 startConnection();
367 } else if (session.isClosed()) {
368 log.debug("Trying to restart the session with {}", session, deviceInfo.getDeviceId());
369 cleanUp();
370 startSession();
371 } else if (channel.isClosed()) {
372 log.debug("Trying to reopen the channel with {}", deviceInfo.getDeviceId());
373 cleanUp();
374 openChannel();
375 }
376 if (subscriptionConnected) {
377 log.debug("Restarting subscription with {}", deviceInfo.getDeviceId());
378 subscriptionConnected = false;
379 startSubscription(notificationFilterSchema);
380 }
Sean Condon7347de92017-07-21 12:17:25 +0100381 } catch (IOException | IllegalStateException e) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700382 log.error("Can't reopen connection for device {}", e.getMessage());
383 throw new NetconfException("Cannot re-open the connection with device" + deviceInfo, e);
384 }
385 }
386
387 private void cleanUp() {
388 //makes sure everything is at a clean state.
389 replies.clear();
390 }
391
392 @Override
393 public String requestSync(String request) throws NetconfException {
394 if (!request.contains(ENDPATTERN)) {
395 request = request + NEW_LINE + ENDPATTERN;
396 }
397 String reply = sendRequest(request);
398 checkReply(reply);
399 return reply;
400 }
401
402 @Override
403 @Deprecated
404 public CompletableFuture<String> request(String request) {
405 return streamHandler.sendMessage(request);
406 }
407
408 private CompletableFuture<String> request(String request, int messageId) {
409 return streamHandler.sendMessage(request, messageId);
410 }
411
412 private String sendRequest(String request) throws NetconfException {
413 return sendRequest(request, false);
414 }
415
416 private String sendRequest(String request, boolean isHello) throws NetconfException {
417 checkAndReestablish();
418 int messageId = -1;
419 if (!isHello) {
420 messageId = messageIdInteger.getAndIncrement();
421 }
422 request = formatRequestMessageId(request, messageId);
423 request = formatXmlHeader(request);
424 CompletableFuture<String> futureReply = request(request, messageId);
425 int replyTimeout = NetconfControllerImpl.netconfReplyTimeout;
426 String rp;
427 try {
428 rp = futureReply.get(replyTimeout, TimeUnit.SECONDS);
Sean Condon7347de92017-07-21 12:17:25 +0100429 replies.remove(messageId); // Why here???
430 } catch (InterruptedException e) {
431 Thread.currentThread().interrupt();
432 throw new NetconfException("Interrupted waiting for reply for request" + request, e);
433 } catch (TimeoutException e) {
434 throw new NetconfException("Timed out waiting for reply for request " + request, e);
435 } catch (ExecutionException e) {
436 log.warn("Closing session {} for {} due to unexpected Error", sessionID, deviceInfo, e);
437 try {
438 session.close();
439 channel.close(); //Closes the socket which should interrupt NetconfStreamThread
440 client.close();
441 } catch (IOException ioe) {
442 log.warn("Error closing session {} on {}", sessionID, deviceInfo, ioe);
443 }
444 NetconfDeviceOutputEvent event = new NetconfDeviceOutputEvent(
445 NetconfDeviceOutputEvent.Type.SESSION_CLOSED,
446 null, "Closed due to unexpected error " + e.getCause(),
447 Optional.of(-1), deviceInfo);
448 publishEvent(event);
449 errorReplies.clear(); // move to cleanUp()?
450 cleanUp();
451
452 throw new NetconfException("Closing session " + sessionID + " for " + deviceInfo +
453 " for request " + request, e);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700454 }
455 log.debug("Result {} from request {} to device {}", rp, request, deviceInfo);
456 return rp.trim();
457 }
458
459 private String formatRequestMessageId(String request, int messageId) {
460 if (request.contains(MESSAGE_ID_STRING)) {
461 //FIXME if application provides his own counting of messages this fails that count
462 request = request.replaceFirst(MESSAGE_ID_STRING + EQUAL + NUMBER_BETWEEN_QUOTES_MATCHER,
463 MESSAGE_ID_STRING + EQUAL + "\"" + messageId + "\"");
464 } else if (!request.contains(MESSAGE_ID_STRING) && !request.contains(HELLO)) {
465 //FIXME find out a better way to enforce the presence of message-id
466 request = request.replaceFirst(END_OF_RPC_OPEN_TAG, "\" " + MESSAGE_ID_STRING + EQUAL + "\""
467 + messageId + "\"" + ">");
468 }
469 return request;
470 }
471
472 private String formatXmlHeader(String request) {
473 if (!request.contains(XML_HEADER)) {
474 //FIXME if application provieds his own XML header of different type there is a clash
475 request = XML_HEADER + "\n" + request;
476 }
477 return request;
478 }
479
480 @Override
481 public String doWrappedRpc(String request) throws NetconfException {
482 StringBuilder rpc = new StringBuilder(XML_HEADER);
483 rpc.append(RPC_OPEN);
484 rpc.append(MESSAGE_ID_STRING);
485 rpc.append(EQUAL);
486 rpc.append("\"");
487 rpc.append(messageIdInteger.get());
488 rpc.append("\" ");
489 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
490 rpc.append(request);
491 rpc.append(RPC_CLOSE).append(NEW_LINE);
492 rpc.append(ENDPATTERN);
493 String reply = sendRequest(rpc.toString());
494 checkReply(reply);
495 return reply;
496 }
497
498 @Override
499 public String get(String request) throws NetconfException {
500 return requestSync(request);
501 }
502
503 @Override
504 public String get(String filterSchema, String withDefaultsMode) throws NetconfException {
505 StringBuilder rpc = new StringBuilder(XML_HEADER);
506 rpc.append(RPC_OPEN);
507 rpc.append(MESSAGE_ID_STRING);
508 rpc.append(EQUAL);
509 rpc.append("\"");
510 rpc.append(messageIdInteger.get());
511 rpc.append("\" ");
512 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
513 rpc.append(GET_OPEN).append(NEW_LINE);
514 if (filterSchema != null) {
515 rpc.append(SUBTREE_FILTER_OPEN).append(NEW_LINE);
516 rpc.append(filterSchema).append(NEW_LINE);
517 rpc.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
518 }
519 if (withDefaultsMode != null) {
520 rpc.append(WITH_DEFAULT_OPEN).append(NETCONF_WITH_DEFAULTS_NAMESPACE).append(">");
521 rpc.append(withDefaultsMode).append(WITH_DEFAULT_CLOSE).append(NEW_LINE);
522 }
523 rpc.append(GET_CLOSE).append(NEW_LINE);
524 rpc.append(RPC_CLOSE).append(NEW_LINE);
525 rpc.append(ENDPATTERN);
526 String reply = sendRequest(rpc.toString());
527 checkReply(reply);
528 return reply;
529 }
530
531 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700532 public String getConfig(DatastoreId netconfTargetConfig) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700533 return getConfig(netconfTargetConfig, null);
534 }
535
536 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700537 public String getConfig(DatastoreId netconfTargetConfig,
538 String configurationSchema) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700539 StringBuilder rpc = new StringBuilder(XML_HEADER);
540 rpc.append("<rpc ");
541 rpc.append(MESSAGE_ID_STRING);
542 rpc.append(EQUAL);
543 rpc.append("\"");
544 rpc.append(messageIdInteger.get());
545 rpc.append("\" ");
546 rpc.append("xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
547 rpc.append("<get-config>\n");
548 rpc.append("<source>\n");
549 rpc.append("<").append(netconfTargetConfig).append("/>");
550 rpc.append("</source>");
551 if (configurationSchema != null) {
552 rpc.append("<filter type=\"subtree\">\n");
553 rpc.append(configurationSchema).append("\n");
554 rpc.append("</filter>\n");
555 }
556 rpc.append("</get-config>\n");
557 rpc.append("</rpc>\n");
558 rpc.append(ENDPATTERN);
559 String reply = sendRequest(rpc.toString());
560 return checkReply(reply) ? reply : "ERROR " + reply;
561 }
562
563 @Override
564 public boolean editConfig(String newConfiguration) throws NetconfException {
565 newConfiguration = newConfiguration + ENDPATTERN;
566 return checkReply(sendRequest(newConfiguration));
567 }
568
569 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700570 public boolean editConfig(DatastoreId netconfTargetConfig,
571 String mode,
572 String newConfiguration)
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700573 throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700574
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700575 newConfiguration = newConfiguration.trim();
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(EDIT_CONFIG_OPEN).append("\n");
585 rpc.append(TARGET_OPEN);
586 rpc.append("<").append(netconfTargetConfig).append("/>");
587 rpc.append(TARGET_CLOSE).append("\n");
588 if (mode != null) {
589 rpc.append(DEFAULT_OPERATION_OPEN);
590 rpc.append(mode);
591 rpc.append(DEFAULT_OPERATION_CLOSE).append("\n");
592 }
593 rpc.append(CONFIG_OPEN).append("\n");
594 rpc.append(newConfiguration);
595 rpc.append(CONFIG_CLOSE).append("\n");
596 rpc.append(EDIT_CONFIG_CLOSE).append("\n");
597 rpc.append(RPC_CLOSE);
598 rpc.append(ENDPATTERN);
599 log.debug(rpc.toString());
600 String reply = sendRequest(rpc.toString());
601 return checkReply(reply);
602 }
603
604 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700605 public boolean copyConfig(DatastoreId destination,
606 DatastoreId source)
607 throws NetconfException {
608 return bareCopyConfig(destination.asXml(), source.asXml());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700609 }
610
611 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700612 public boolean copyConfig(DatastoreId netconfTargetConfig,
613 String newConfiguration)
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700614 throws NetconfException {
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700615 return bareCopyConfig(netconfTargetConfig.asXml(),
616 normalizeCopyConfigParam(newConfiguration));
617 }
618
619 @Override
620 public boolean copyConfig(String netconfTargetConfig,
621 String newConfiguration) throws NetconfException {
622 return bareCopyConfig(normalizeCopyConfigParam(netconfTargetConfig),
623 normalizeCopyConfigParam(newConfiguration));
624 }
625
626 /**
627 * Normalize String parameter passed to copy-config API.
628 * <p>
629 * Provided for backward compatibility purpose
630 *
631 * @param input passed to copyConfig API
632 * @return XML likely to be suitable for copy-config source or target
633 */
634 private static CharSequence normalizeCopyConfigParam(String input) {
635 input = input.trim();
636 if (input.startsWith("<url")) {
637 return input;
638 } else if (!input.startsWith("<")) {
639 // assume it is a datastore name
640 return DatastoreId.datastore(input).asXml();
641 } else if (!input.startsWith("<config>")) {
642 return "<config>" + input + "</config>";
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700643 }
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700644 return input;
645 }
646
647 private boolean bareCopyConfig(CharSequence target,
648 CharSequence source)
649 throws NetconfException {
650
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700651 StringBuilder rpc = new StringBuilder(XML_HEADER);
652 rpc.append(RPC_OPEN);
653 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
654 rpc.append("<copy-config>");
655 rpc.append("<target>");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700656 rpc.append(target);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700657 rpc.append("</target>");
658 rpc.append("<source>");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700659 rpc.append(source);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700660 rpc.append("</source>");
661 rpc.append("</copy-config>");
662 rpc.append("</rpc>");
663 rpc.append(ENDPATTERN);
664 return checkReply(sendRequest(rpc.toString()));
665 }
666
667 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700668 public boolean deleteConfig(DatastoreId netconfTargetConfig) throws NetconfException {
669 if (netconfTargetConfig.equals(DatastoreId.RUNNING)) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700670 log.warn("Target configuration for delete operation can't be \"running\"",
671 netconfTargetConfig);
672 return false;
673 }
674 StringBuilder rpc = new StringBuilder(XML_HEADER);
675 rpc.append("<rpc>");
676 rpc.append("<delete-config>");
677 rpc.append("<target>");
678 rpc.append("<").append(netconfTargetConfig).append("/>");
679 rpc.append("</target>");
680 rpc.append("</delete-config>");
681 rpc.append("</rpc>");
682 rpc.append(ENDPATTERN);
683 return checkReply(sendRequest(rpc.toString()));
684 }
685
686 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700687 public boolean lock(DatastoreId configType) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700688 StringBuilder rpc = new StringBuilder(XML_HEADER);
689 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
690 rpc.append("<lock>");
691 rpc.append("<target>");
692 rpc.append("<");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700693 rpc.append(configType.id());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700694 rpc.append("/>");
695 rpc.append("</target>");
696 rpc.append("</lock>");
697 rpc.append("</rpc>");
698 rpc.append(ENDPATTERN);
699 String lockReply = sendRequest(rpc.toString());
700 return checkReply(lockReply);
701 }
702
703 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700704 public boolean unlock(DatastoreId configType) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700705 StringBuilder rpc = new StringBuilder(XML_HEADER);
706 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
707 rpc.append("<unlock>");
708 rpc.append("<target>");
709 rpc.append("<");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700710 rpc.append(configType.id());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700711 rpc.append("/>");
712 rpc.append("</target>");
713 rpc.append("</unlock>");
714 rpc.append("</rpc>");
715 rpc.append(ENDPATTERN);
716 String unlockReply = sendRequest(rpc.toString());
717 return checkReply(unlockReply);
718 }
719
720 @Override
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700721 public boolean close() throws NetconfException {
722 return close(false);
723 }
724
725 private boolean close(boolean force) throws NetconfException {
726 StringBuilder rpc = new StringBuilder();
727 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">");
728 if (force) {
729 rpc.append("<kill-session/>");
730 } else {
731 rpc.append("<close-session/>");
732 }
733 rpc.append("</rpc>");
734 rpc.append(ENDPATTERN);
735 return checkReply(sendRequest(rpc.toString())) || close(true);
736 }
737
738 @Override
739 public String getSessionId() {
740 return sessionID;
741 }
742
743 @Override
744 public Set<String> getDeviceCapabilitiesSet() {
745 return Collections.unmodifiableSet(deviceCapabilities);
746 }
747
748 @Deprecated
749 @Override
750 public String getServerCapabilities() {
751 return serverHelloResponseOld;
752 }
753
754 @Deprecated
755 @Override
756 public void setDeviceCapabilities(List<String> capabilities) {
757 onosCapabilities = capabilities;
758 }
759
760 @Override
761 public void setOnosCapabilities(Iterable<String> capabilities) {
762 onosCapabilities = capabilities;
763 }
764
765
766 @Override
767 public void addDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
768 streamHandler.addDeviceEventListener(listener);
769 primaryListeners.add(listener);
770 }
771
772 @Override
773 public void removeDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
774 primaryListeners.remove(listener);
775 streamHandler.removeDeviceEventListener(listener);
776 }
777
778 private boolean checkReply(String reply) throws NetconfException {
779 if (reply != null) {
780 if (!reply.contains("<rpc-error>")) {
781 log.debug("Device {} sent reply {}", deviceInfo, reply);
782 return true;
783 } else if (reply.contains("<ok/>")
784 || (reply.contains("<rpc-error>")
785 && reply.contains("warning"))) {
786 log.debug("Device {} sent reply {}", deviceInfo, reply);
787 return true;
788 }
789 }
790 log.warn("Device {} has error in reply {}", deviceInfo, reply);
791 return false;
792 }
793
Sean Condon7347de92017-07-21 12:17:25 +0100794 protected void publishEvent(NetconfDeviceOutputEvent event) {
795 primaryListeners.forEach(lsnr -> {
796 if (lsnr.isRelevant(event)) {
797 lsnr.event(event);
798 }
799 });
800 }
801
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700802 static class NotificationSession extends NetconfSessionMinaImpl {
803
804 private String notificationFilter;
805
806 NotificationSession(NetconfDeviceInfo deviceInfo)
807 throws NetconfException {
808 super(deviceInfo);
809 }
810
811 @Override
812 protected void startSubscriptionStream(String filterSchema)
813 throws NetconfException {
814
815 notificationFilter = filterSchema;
816 requestSync(createSubscriptionString(filterSchema));
817 }
818
819 @Override
820 public String toString() {
821 return MoreObjects.toStringHelper(getClass())
822 .add("deviceInfo", deviceInfo)
823 .add("sessionID", getSessionId())
824 .add("notificationFilter", notificationFilter)
825 .toString();
826 }
827 }
828
829 /**
830 * Listener attached to child session for notification streaming.
831 * <p>
832 * Forwards all notification event from child session to primary session
833 * listeners.
834 */
835 private final class NotificationForwarder
836 implements NetconfDeviceOutputEventListener {
837
838 @Override
839 public boolean isRelevant(NetconfDeviceOutputEvent event) {
840 return event.type() == Type.DEVICE_NOTIFICATION;
841 }
842
843 @Override
844 public void event(NetconfDeviceOutputEvent event) {
Sean Condon7347de92017-07-21 12:17:25 +0100845 publishEvent(event);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700846 }
847 }
848
849 public class NetconfSessionDelegateImpl implements NetconfSessionDelegate {
850
851 @Override
852 public void notify(NetconfDeviceOutputEvent event) {
853 Optional<Integer> messageId = event.getMessageID();
854 log.debug("messageID {}, waiting replies messageIDs {}", messageId,
855 replies.keySet());
856 if (!messageId.isPresent()) {
857 errorReplies.add(event.getMessagePayload());
858 log.error("Device {} sent error reply {}",
859 event.getDeviceInfo(), event.getMessagePayload());
860 return;
861 }
862 CompletableFuture<String> completedReply =
Sean Condon7347de92017-07-21 12:17:25 +0100863 replies.get(messageId.get()); // remove(..)?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700864 if (completedReply != null) {
865 completedReply.complete(event.getMessagePayload());
866 }
867 }
868 }
869
870 public static class MinaSshNetconfSessionFactory implements NetconfSessionFactory {
871
872 @Override
873 public NetconfSession createNetconfSession(NetconfDeviceInfo netconfDeviceInfo) throws NetconfException {
874 return new NetconfSessionMinaImpl(netconfDeviceInfo);
875 }
876 }
877}