blob: 846ec26edcd48ce9f30baf07afaa4c41cf70c0b3 [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 HIGUCHI6e6c26e2017-09-06 14:25:57 -070035import org.onlab.util.SharedExecutors;
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -070036import org.onosproject.netconf.DatastoreId;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070037import org.onosproject.netconf.NetconfDeviceInfo;
38import org.onosproject.netconf.NetconfDeviceOutputEvent;
39import org.onosproject.netconf.NetconfDeviceOutputEvent.Type;
40import org.onosproject.netconf.NetconfDeviceOutputEventListener;
41import org.onosproject.netconf.NetconfException;
42import org.onosproject.netconf.NetconfSession;
43import org.onosproject.netconf.NetconfSessionFactory;
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -070044import org.onosproject.netconf.NetconfTransportException;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070045import org.slf4j.Logger;
Yuta HIGUCHI15677982017-08-16 15:50:29 -070046import static java.nio.charset.StandardCharsets.UTF_8;
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -070047import static org.slf4j.LoggerFactory.getLogger;
Yuta HIGUCHI15677982017-08-16 15:50:29 -070048
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
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -070080 private static final Logger log = getLogger(NetconfSessionMinaImpl.class);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070081
Yuta HIGUCHI371667d2017-09-05 17:30:51 -070082 /**
83 * NC 1.0, RFC4742 EOM sequence.
84 */
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070085 private static final String ENDPATTERN = "]]>]]>";
86 private static final String MESSAGE_ID_STRING = "message-id";
87 private static final String HELLO = "<hello";
88 private static final String NEW_LINE = "\n";
89 private static final String END_OF_RPC_OPEN_TAG = "\">";
90 private static final String EQUAL = "=";
91 private static final String NUMBER_BETWEEN_QUOTES_MATCHER = "\"+([0-9]+)+\"";
92 private static final String RPC_OPEN = "<rpc ";
93 private static final String RPC_CLOSE = "</rpc>";
94 private static final String GET_OPEN = "<get>";
95 private static final String GET_CLOSE = "</get>";
96 private static final String WITH_DEFAULT_OPEN = "<with-defaults ";
97 private static final String WITH_DEFAULT_CLOSE = "</with-defaults>";
98 private static final String DEFAULT_OPERATION_OPEN = "<default-operation>";
99 private static final String DEFAULT_OPERATION_CLOSE = "</default-operation>";
100 private static final String SUBTREE_FILTER_OPEN = "<filter type=\"subtree\">";
101 private static final String SUBTREE_FILTER_CLOSE = "</filter>";
102 private static final String EDIT_CONFIG_OPEN = "<edit-config>";
103 private static final String EDIT_CONFIG_CLOSE = "</edit-config>";
104 private static final String TARGET_OPEN = "<target>";
105 private static final String TARGET_CLOSE = "</target>";
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700106 // FIXME hard coded namespace nc
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700107 private static final String CONFIG_OPEN = "<config xmlns:nc=\"urn:ietf:params:xml:ns:netconf:base:1.0\">";
108 private static final String CONFIG_CLOSE = "</config>";
109 private static final String XML_HEADER =
110 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
111 private static final String NETCONF_BASE_NAMESPACE =
112 "xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\"";
113 private static final String NETCONF_WITH_DEFAULTS_NAMESPACE =
114 "xmlns=\"urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults\"";
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700115 // FIXME hard coded namespace base10
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700116 private static final String SUBSCRIPTION_SUBTREE_FILTER_OPEN =
117 "<filter xmlns:base10=\"urn:ietf:params:xml:ns:netconf:base:1.0\" base10:type=\"subtree\">";
118
119 private static final String INTERLEAVE_CAPABILITY_STRING = "urn:ietf:params:netconf:capability:interleave:1.0";
120
121 private static final String CAPABILITY_REGEX = "<capability>\\s*(.*?)\\s*</capability>";
122 private static final Pattern CAPABILITY_REGEX_PATTERN = Pattern.compile(CAPABILITY_REGEX);
123
124 private static final String SESSION_ID_REGEX = "<session-id>\\s*(.*?)\\s*</session-id>";
125 private static final Pattern SESSION_ID_REGEX_PATTERN = Pattern.compile(SESSION_ID_REGEX);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200126 private static final String HASH = "#";
127 private static final String LF = "\n";
128 private static final String MSGLEN_REGEX_PATTERN = "\n#\\d+\n";
129 private static final String NETCONF_10_CAPABILITY = "urn:ietf:params:netconf:base:1.0";
130 private static final String NETCONF_11_CAPABILITY = "urn:ietf:params:netconf:base:1.1";
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700131
132 private String sessionID;
133 private final AtomicInteger messageIdInteger = new AtomicInteger(1);
134 protected final NetconfDeviceInfo deviceInfo;
135 private Iterable<String> onosCapabilities =
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200136 ImmutableList.of(NETCONF_10_CAPABILITY, NETCONF_11_CAPABILITY);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700137
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700138 private final Set<String> deviceCapabilities = new LinkedHashSet<>();
139 private NetconfStreamHandler streamHandler;
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700140 // FIXME ONOS-7019 key type should be revised to a String, see RFC6241
141 /**
142 * Message-ID and corresponding Future waiting for response.
143 */
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700144 private Map<Integer, CompletableFuture<String>> replies;
Sean Condon7347de92017-07-21 12:17:25 +0100145 private List<String> errorReplies; // Not sure why we need this?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700146 private boolean subscriptionConnected = false;
147 private String notificationFilterSchema = null;
148
149 private final Collection<NetconfDeviceOutputEventListener> primaryListeners =
150 new CopyOnWriteArrayList<>();
151 private final Collection<NetconfSession> children =
152 new CopyOnWriteArrayList<>();
153
Sean Condon54d82432017-07-26 22:27:25 +0100154 private int connectTimeout;
155 private int replyTimeout;
156 private int idleTimeout;
157
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700158
159 private ClientChannel channel = null;
160 private ClientSession session = null;
161 private SshClient client = null;
162
163
164 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo) throws NetconfException {
165 this.deviceInfo = deviceInfo;
166 replies = new ConcurrentHashMap<>();
167 errorReplies = new ArrayList<>();
Sean Condon54d82432017-07-26 22:27:25 +0100168
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700169 // FIXME should not immediately start session on construction
170 // setOnosCapabilities() is useless due to this behavior
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700171 startConnection();
172 }
173
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200174 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo, List<String> capabilities) throws NetconfException {
175 this.deviceInfo = deviceInfo;
176 replies = new ConcurrentHashMap<>();
177 errorReplies = new ArrayList<>();
178 setOnosCapabilities(capabilities);
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700179 // FIXME should not immediately start session on construction
180 // setOnosCapabilities() is useless due to this behavior
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200181 startConnection();
182 }
183
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700184 private void startConnection() throws NetconfException {
Sean Condon54d82432017-07-26 22:27:25 +0100185 connectTimeout = deviceInfo.getConnectTimeoutSec().orElse(
186 NetconfControllerImpl.netconfConnectTimeout);
187 replyTimeout = deviceInfo.getReplyTimeoutSec().orElse(
188 NetconfControllerImpl.netconfReplyTimeout);
189 idleTimeout = deviceInfo.getIdleTimeoutSec().orElse(
190 NetconfControllerImpl.netconfIdleTimeout);
191 log.info("Connecting to {} with timeouts C:{}, R:{}, I:{}", deviceInfo,
192 connectTimeout, replyTimeout, idleTimeout);
193
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700194 try {
195 startClient();
196 } catch (IOException e) {
197 throw new NetconfException("Failed to establish SSH with device " + deviceInfo, e);
198 }
199 }
200
201 private void startClient() throws IOException {
Yuta HIGUCHI2ee4fba2018-06-12 16:21:06 -0700202 log.info("Creating NETCONF session to {}",
203 deviceInfo.getDeviceId());
204
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700205 client = SshClient.setUpDefaultClient();
Sean Condon7347de92017-07-21 12:17:25 +0100206 client.getProperties().putIfAbsent(FactoryManager.IDLE_TIMEOUT,
Sean Condon54d82432017-07-26 22:27:25 +0100207 TimeUnit.SECONDS.toMillis(idleTimeout));
Sean Condon7347de92017-07-21 12:17:25 +0100208 client.getProperties().putIfAbsent(FactoryManager.NIO2_READ_TIMEOUT,
Sean Condon54d82432017-07-26 22:27:25 +0100209 TimeUnit.SECONDS.toMillis(idleTimeout + 15L));
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700210 client.start();
211 client.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
212 startSession();
213 }
214
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700215 // FIXME blocking
216 @Deprecated
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700217 private void startSession() throws IOException {
218 final ConnectFuture connectFuture;
219 connectFuture = client.connect(deviceInfo.name(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200220 deviceInfo.ip().toString(),
221 deviceInfo.port())
Sean Condon54d82432017-07-26 22:27:25 +0100222 .verify(connectTimeout, TimeUnit.SECONDS);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700223 session = connectFuture.getSession();
224 //Using the device ssh key if possible
225 if (deviceInfo.getKey() != null) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700226 try (PEMParser pemParser = new PEMParser(new CharArrayReader(deviceInfo.getKey()))) {
227 JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME);
228 try {
229 KeyPair kp = converter.getKeyPair((PEMKeyPair) pemParser.readObject());
230 session.addPublicKeyIdentity(kp);
231 } catch (IOException e) {
232 throw new NetconfException("Failed to authenticate session with device " +
233 deviceInfo + "check key to be a valid key", e);
234 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700235 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700236 } else {
237 session.addPasswordIdentity(deviceInfo.password());
238 }
Sean Condon54d82432017-07-26 22:27:25 +0100239 session.auth().verify(connectTimeout, TimeUnit.SECONDS);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700240 Set<ClientSession.ClientSessionEvent> event = session.waitFor(
241 ImmutableSet.of(ClientSession.ClientSessionEvent.WAIT_AUTH,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200242 ClientSession.ClientSessionEvent.CLOSED,
243 ClientSession.ClientSessionEvent.AUTHED), 0);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700244
245 if (!event.contains(ClientSession.ClientSessionEvent.AUTHED)) {
246 log.debug("Session closed {} {}", event, session.isClosed());
247 throw new NetconfException("Failed to authenticate session with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200248 deviceInfo + "check the user/pwd or key");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700249 }
250 openChannel();
251 }
252
253 private PublicKey getPublicKey(byte[] keyBytes, String type)
254 throws NoSuchAlgorithmException, InvalidKeySpecException {
255
256 X509EncodedKeySpec spec =
257 new X509EncodedKeySpec(keyBytes);
258 KeyFactory kf = KeyFactory.getInstance(type);
259 return kf.generatePublic(spec);
260 }
261
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700262 // FIXME blocking
263 @Deprecated
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700264 private void openChannel() throws IOException {
265 channel = session.createSubsystemChannel("netconf");
266 OpenFuture channelFuture = channel.open();
Sean Condon54d82432017-07-26 22:27:25 +0100267 if (channelFuture.await(connectTimeout, TimeUnit.SECONDS)) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700268 if (channelFuture.isOpened()) {
269 streamHandler = new NetconfStreamThread(channel.getInvertedOut(), channel.getInvertedIn(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200270 channel.getInvertedErr(), deviceInfo,
271 new NetconfSessionDelegateImpl(), replies);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700272 } else {
273 throw new NetconfException("Failed to open channel with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200274 deviceInfo);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700275 }
276 sendHello();
277 }
278 }
279
280
281 @Beta
282 protected void startSubscriptionStream(String filterSchema) throws NetconfException {
283 boolean openNewSession = false;
284 if (!deviceCapabilities.contains(INTERLEAVE_CAPABILITY_STRING)) {
285 log.info("Device {} doesn't support interleave, creating child session", deviceInfo);
286 openNewSession = true;
287
288 } else if (subscriptionConnected &&
289 notificationFilterSchema != null &&
290 !Objects.equal(filterSchema, notificationFilterSchema)) {
291 // interleave supported and existing filter is NOT "no filtering"
292 // and was requested with different filtering schema
293 log.info("Cannot use existing session for subscription {} ({})",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200294 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700295 openNewSession = true;
296 }
297
298 if (openNewSession) {
299 log.info("Creating notification session to {} with filter {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200300 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700301 NetconfSession child = new NotificationSession(deviceInfo);
302
303 child.addDeviceOutputListener(new NotificationForwarder());
304
305 child.startSubscription(filterSchema);
306 children.add(child);
307 return;
308 }
309
310 // request to start interleaved notification session
311 String reply = sendRequest(createSubscriptionString(filterSchema));
312 if (!checkReply(reply)) {
313 throw new NetconfException("Subscription not successful with device "
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200314 + deviceInfo + " with reply " + reply);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700315 }
316 subscriptionConnected = true;
317 }
318
319 @Override
320 public void startSubscription() throws NetconfException {
321 if (!subscriptionConnected) {
322 startSubscriptionStream(null);
323 }
324 streamHandler.setEnableNotifications(true);
325 }
326
327 @Beta
328 @Override
329 public void startSubscription(String filterSchema) throws NetconfException {
330 if (!subscriptionConnected) {
331 notificationFilterSchema = filterSchema;
332 startSubscriptionStream(filterSchema);
333 }
334 streamHandler.setEnableNotifications(true);
335 }
336
337 @Beta
338 protected String createSubscriptionString(String filterSchema) {
339 StringBuilder subscriptionbuffer = new StringBuilder();
340 subscriptionbuffer.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
341 subscriptionbuffer.append(" <create-subscription\n");
342 subscriptionbuffer.append("xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">\n");
343 // FIXME Only subtree filtering supported at the moment.
344 if (filterSchema != null) {
345 subscriptionbuffer.append(" ");
346 subscriptionbuffer.append(SUBSCRIPTION_SUBTREE_FILTER_OPEN).append(NEW_LINE);
347 subscriptionbuffer.append(filterSchema).append(NEW_LINE);
348 subscriptionbuffer.append(" ");
349 subscriptionbuffer.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
350 }
351 subscriptionbuffer.append(" </create-subscription>\n");
352 subscriptionbuffer.append("</rpc>\n");
353 subscriptionbuffer.append(ENDPATTERN);
354 return subscriptionbuffer.toString();
355 }
356
357 @Override
358 public void endSubscription() throws NetconfException {
359 if (subscriptionConnected) {
360 streamHandler.setEnableNotifications(false);
361 } else {
362 throw new NetconfException("Subscription does not exist.");
363 }
364 }
365
366 private void sendHello() throws NetconfException {
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700367 String serverHelloResponse = sendRequest(createHelloString(), true);
368 Matcher capabilityMatcher = CAPABILITY_REGEX_PATTERN.matcher(serverHelloResponse);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700369 while (capabilityMatcher.find()) {
370 deviceCapabilities.add(capabilityMatcher.group(1));
371 }
372 sessionID = String.valueOf(-1);
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700373 Matcher sessionIDMatcher = SESSION_ID_REGEX_PATTERN.matcher(serverHelloResponse);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700374 if (sessionIDMatcher.find()) {
375 sessionID = sessionIDMatcher.group(1);
376 } else {
377 throw new NetconfException("Missing SessionID in server hello " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200378 "reponse.");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700379 }
380
381 }
382
383 private String createHelloString() {
384 StringBuilder hellobuffer = new StringBuilder();
385 hellobuffer.append(XML_HEADER);
386 hellobuffer.append("\n");
387 hellobuffer.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
388 hellobuffer.append(" <capabilities>\n");
389 onosCapabilities.forEach(
390 cap -> hellobuffer.append(" <capability>")
391 .append(cap)
392 .append("</capability>\n"));
393 hellobuffer.append(" </capabilities>\n");
394 hellobuffer.append("</hello>\n");
395 hellobuffer.append(ENDPATTERN);
396 return hellobuffer.toString();
397
398 }
399
400 @Override
401 public void checkAndReestablish() throws NetconfException {
402 try {
403 if (client.isClosed()) {
404 log.debug("Trying to restart the whole SSH connection with {}", deviceInfo.getDeviceId());
405 cleanUp();
406 startConnection();
407 } else if (session.isClosed()) {
408 log.debug("Trying to restart the session with {}", session, deviceInfo.getDeviceId());
409 cleanUp();
410 startSession();
411 } else if (channel.isClosed()) {
412 log.debug("Trying to reopen the channel with {}", deviceInfo.getDeviceId());
413 cleanUp();
414 openChannel();
Andrea Campanella856f3132017-10-23 15:46:36 +0200415 } else {
416 return;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700417 }
418 if (subscriptionConnected) {
419 log.debug("Restarting subscription with {}", deviceInfo.getDeviceId());
420 subscriptionConnected = false;
421 startSubscription(notificationFilterSchema);
422 }
Sean Condon7347de92017-07-21 12:17:25 +0100423 } catch (IOException | IllegalStateException e) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700424 log.error("Can't reopen connection for device {}", e.getMessage());
425 throw new NetconfException("Cannot re-open the connection with device" + deviceInfo, e);
426 }
427 }
428
429 private void cleanUp() {
430 //makes sure everything is at a clean state.
431 replies.clear();
432 }
433
434 @Override
435 public String requestSync(String request) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700436 String reply = sendRequest(request);
437 checkReply(reply);
438 return reply;
439 }
440
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200441
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700442 // FIXME rename to align with what it actually do
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200443 /**
444 * Validate and format netconf message.
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700445 * - NC1.0 if no EOM sequence present on {@code message}, append.
446 * - NC1.1 chunk-encode given message unless it already is chunk encoded
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200447 *
448 * @param message to format
449 * @return formated message
450 */
451 private String formatNetconfMessage(String message) {
452 if (deviceCapabilities.contains(NETCONF_11_CAPABILITY)) {
453 message = formatChunkedMessage(message);
454 } else {
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700455 if (!message.endsWith(ENDPATTERN)) {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200456 message = message + NEW_LINE + ENDPATTERN;
457 }
458 }
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700459 return message;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200460 }
461
462 /**
463 * Validate and format message according to chunked framing mechanism.
464 *
465 * @param message to format
466 * @return formated message
467 */
468 private String formatChunkedMessage(String message) {
469 if (message.endsWith(ENDPATTERN)) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700470 // message given had Netconf 1.0 EOM pattern -> remove
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200471 message = message.substring(0, message.length() - ENDPATTERN.length());
472 }
473 if (!message.startsWith(LF + HASH)) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700474 // chunk encode message
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700475 message = LF + HASH + message.getBytes(UTF_8).length + LF + message + LF + HASH + HASH + LF;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200476 }
477 return message;
478 }
479
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700480 @Override
481 @Deprecated
482 public CompletableFuture<String> request(String request) {
483 return streamHandler.sendMessage(request);
484 }
485
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700486 /**
487 * {@inheritDoc}
488 * <p>
489 * FIXME Note: as of 1.12.0
490 * {@code request} must not include message-id, this method will assign
491 * and insert message-id on it's own.
492 * Will require ONOS-7019 to remove this limitation.
493 */
494 @Override
495 public CompletableFuture<String> rpc(String request) {
496
497 String rpc = request;
498 // - assign message-id
499 int msgId = messageIdInteger.incrementAndGet();
500 // - re-write request to insert message-id
501 // FIXME avoid using formatRequestMessageId
502 rpc = formatRequestMessageId(rpc, msgId);
503 // - ensure it contains XML header
504 rpc = formatXmlHeader(rpc);
505 // - use chunked framing if talking to NC 1.1 device
506 // FIXME avoid using formatNetconfMessage
507 rpc = formatNetconfMessage(rpc);
508
509 // TODO session liveness check & recovery
510
511 log.debug("Sending {} to {}", rpc, this.deviceInfo.getDeviceId());
512 return streamHandler.sendMessage(rpc, msgId)
513 .handleAsync((reply, t) -> {
514 if (t != null) {
515 // secure transport-layer error
516 // cannot use NetconfException, which is
517 // checked Exception.
518 throw new NetconfTransportException(t);
519 } else {
520 // FIXME avoid using checkReply, error handling is weird
521 checkReply(reply);
522 return reply;
523 }
524 }, SharedExecutors.getPoolThreadExecutor());
525 }
526
Sean Condon54d82432017-07-26 22:27:25 +0100527 @Override
528 public int timeoutConnectSec() {
529 return connectTimeout;
530 }
531
532 @Override
533 public int timeoutReplySec() {
534 return replyTimeout;
535 }
536
537 @Override
538 public int timeoutIdleSec() {
539 return idleTimeout;
540 }
541
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700542 private CompletableFuture<String> request(String request, int messageId) {
543 return streamHandler.sendMessage(request, messageId);
544 }
545
546 private String sendRequest(String request) throws NetconfException {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700547 // FIXME probably chunk-encoding too early
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200548 request = formatNetconfMessage(request);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700549 return sendRequest(request, false);
550 }
551
552 private String sendRequest(String request, boolean isHello) throws NetconfException {
553 checkAndReestablish();
554 int messageId = -1;
555 if (!isHello) {
556 messageId = messageIdInteger.getAndIncrement();
557 }
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700558 // FIXME potentially re-writing chunked encoded String?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700559 request = formatXmlHeader(request);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200560 request = formatRequestMessageId(request, messageId);
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700561 log.debug("Sending request to NETCONF with timeout {} for {}",
562 replyTimeout, deviceInfo.name());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700563 CompletableFuture<String> futureReply = request(request, messageId);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700564 String rp;
565 try {
566 rp = futureReply.get(replyTimeout, TimeUnit.SECONDS);
Sean Condon7347de92017-07-21 12:17:25 +0100567 replies.remove(messageId); // Why here???
568 } catch (InterruptedException e) {
569 Thread.currentThread().interrupt();
570 throw new NetconfException("Interrupted waiting for reply for request" + request, e);
571 } catch (TimeoutException e) {
Sean Condon54d82432017-07-26 22:27:25 +0100572 throw new NetconfException("Timed out waiting for reply for request " +
573 request + " after " + replyTimeout + " sec.", e);
Sean Condon7347de92017-07-21 12:17:25 +0100574 } catch (ExecutionException e) {
575 log.warn("Closing session {} for {} due to unexpected Error", sessionID, deviceInfo, e);
576 try {
577 session.close();
578 channel.close(); //Closes the socket which should interrupt NetconfStreamThread
579 client.close();
580 } catch (IOException ioe) {
581 log.warn("Error closing session {} on {}", sessionID, deviceInfo, ioe);
582 }
583 NetconfDeviceOutputEvent event = new NetconfDeviceOutputEvent(
584 NetconfDeviceOutputEvent.Type.SESSION_CLOSED,
585 null, "Closed due to unexpected error " + e.getCause(),
586 Optional.of(-1), deviceInfo);
587 publishEvent(event);
588 errorReplies.clear(); // move to cleanUp()?
589 cleanUp();
590
591 throw new NetconfException("Closing session " + sessionID + " for " + deviceInfo +
592 " for request " + request, e);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700593 }
594 log.debug("Result {} from request {} to device {}", rp, request, deviceInfo);
595 return rp.trim();
596 }
597
598 private String formatRequestMessageId(String request, int messageId) {
599 if (request.contains(MESSAGE_ID_STRING)) {
600 //FIXME if application provides his own counting of messages this fails that count
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700601 // FIXME assumes message-id is integer. RFC6241 allows anything as long as it is allowed in XML
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700602 request = request.replaceFirst(MESSAGE_ID_STRING + EQUAL + NUMBER_BETWEEN_QUOTES_MATCHER,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200603 MESSAGE_ID_STRING + EQUAL + "\"" + messageId + "\"");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700604 } else if (!request.contains(MESSAGE_ID_STRING) && !request.contains(HELLO)) {
605 //FIXME find out a better way to enforce the presence of message-id
606 request = request.replaceFirst(END_OF_RPC_OPEN_TAG, "\" " + MESSAGE_ID_STRING + EQUAL + "\""
607 + messageId + "\"" + ">");
608 }
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700609 request = updateRequestLength(request);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200610 return request;
611 }
612
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700613 private String updateRequestLength(String request) {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200614 if (request.contains(LF + HASH + HASH + LF)) {
615 int oldLen = Integer.parseInt(request.split(HASH)[1].split(LF)[0]);
616 String rpcWithEnding = request.substring(request.indexOf('<'));
617 String firstBlock = request.split(MSGLEN_REGEX_PATTERN)[1].split(LF + HASH + HASH + LF)[0];
618 int newLen = 0;
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700619 newLen = firstBlock.getBytes(UTF_8).length;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200620 if (oldLen != newLen) {
621 return LF + HASH + newLen + LF + rpcWithEnding;
622 }
623 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700624 return request;
625 }
626
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700627 /**
628 * Ensures xml start directive/declaration appears in the {@code request}.
629 * @param request RPC request message
630 * @return XML RPC message
631 */
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700632 private String formatXmlHeader(String request) {
Sean Condon2d647172017-09-19 12:29:13 +0100633 if (!request.contains(XML_HEADER)) {
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700634 //FIXME if application provides his own XML header of different type there is a clash
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200635 if (request.startsWith(LF + HASH)) {
636 request = request.split("<")[0] + XML_HEADER + request.substring(request.split("<")[0].length());
637 } else {
638 request = XML_HEADER + "\n" + request;
639 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700640 }
641 return request;
642 }
643
644 @Override
645 public String doWrappedRpc(String request) throws NetconfException {
646 StringBuilder rpc = new StringBuilder(XML_HEADER);
647 rpc.append(RPC_OPEN);
648 rpc.append(MESSAGE_ID_STRING);
649 rpc.append(EQUAL);
650 rpc.append("\"");
651 rpc.append(messageIdInteger.get());
652 rpc.append("\" ");
653 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
654 rpc.append(request);
655 rpc.append(RPC_CLOSE).append(NEW_LINE);
656 rpc.append(ENDPATTERN);
657 String reply = sendRequest(rpc.toString());
658 checkReply(reply);
659 return reply;
660 }
661
662 @Override
663 public String get(String request) throws NetconfException {
664 return requestSync(request);
665 }
666
667 @Override
668 public String get(String filterSchema, String withDefaultsMode) throws NetconfException {
669 StringBuilder rpc = new StringBuilder(XML_HEADER);
670 rpc.append(RPC_OPEN);
671 rpc.append(MESSAGE_ID_STRING);
672 rpc.append(EQUAL);
673 rpc.append("\"");
674 rpc.append(messageIdInteger.get());
675 rpc.append("\" ");
676 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
677 rpc.append(GET_OPEN).append(NEW_LINE);
678 if (filterSchema != null) {
679 rpc.append(SUBTREE_FILTER_OPEN).append(NEW_LINE);
680 rpc.append(filterSchema).append(NEW_LINE);
681 rpc.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
682 }
683 if (withDefaultsMode != null) {
684 rpc.append(WITH_DEFAULT_OPEN).append(NETCONF_WITH_DEFAULTS_NAMESPACE).append(">");
685 rpc.append(withDefaultsMode).append(WITH_DEFAULT_CLOSE).append(NEW_LINE);
686 }
687 rpc.append(GET_CLOSE).append(NEW_LINE);
688 rpc.append(RPC_CLOSE).append(NEW_LINE);
689 rpc.append(ENDPATTERN);
690 String reply = sendRequest(rpc.toString());
691 checkReply(reply);
692 return reply;
693 }
694
695 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700696 public String getConfig(DatastoreId netconfTargetConfig) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700697 return getConfig(netconfTargetConfig, null);
698 }
699
700 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700701 public String getConfig(DatastoreId netconfTargetConfig,
702 String configurationSchema) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700703 StringBuilder rpc = new StringBuilder(XML_HEADER);
704 rpc.append("<rpc ");
705 rpc.append(MESSAGE_ID_STRING);
706 rpc.append(EQUAL);
707 rpc.append("\"");
708 rpc.append(messageIdInteger.get());
709 rpc.append("\" ");
710 rpc.append("xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
711 rpc.append("<get-config>\n");
712 rpc.append("<source>\n");
713 rpc.append("<").append(netconfTargetConfig).append("/>");
714 rpc.append("</source>");
715 if (configurationSchema != null) {
716 rpc.append("<filter type=\"subtree\">\n");
717 rpc.append(configurationSchema).append("\n");
718 rpc.append("</filter>\n");
719 }
720 rpc.append("</get-config>\n");
721 rpc.append("</rpc>\n");
722 rpc.append(ENDPATTERN);
723 String reply = sendRequest(rpc.toString());
724 return checkReply(reply) ? reply : "ERROR " + reply;
725 }
726
727 @Override
728 public boolean editConfig(String newConfiguration) throws NetconfException {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200729 if (!newConfiguration.endsWith(ENDPATTERN)) {
730 newConfiguration = newConfiguration + ENDPATTERN;
731 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700732 return checkReply(sendRequest(newConfiguration));
733 }
734
735 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700736 public boolean editConfig(DatastoreId netconfTargetConfig,
737 String mode,
738 String newConfiguration)
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700739 throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700740
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700741 newConfiguration = newConfiguration.trim();
742 StringBuilder rpc = new StringBuilder(XML_HEADER);
743 rpc.append(RPC_OPEN);
744 rpc.append(MESSAGE_ID_STRING);
745 rpc.append(EQUAL);
746 rpc.append("\"");
747 rpc.append(messageIdInteger.get());
748 rpc.append("\" ");
749 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
750 rpc.append(EDIT_CONFIG_OPEN).append("\n");
751 rpc.append(TARGET_OPEN);
752 rpc.append("<").append(netconfTargetConfig).append("/>");
753 rpc.append(TARGET_CLOSE).append("\n");
754 if (mode != null) {
755 rpc.append(DEFAULT_OPERATION_OPEN);
756 rpc.append(mode);
757 rpc.append(DEFAULT_OPERATION_CLOSE).append("\n");
758 }
759 rpc.append(CONFIG_OPEN).append("\n");
760 rpc.append(newConfiguration);
761 rpc.append(CONFIG_CLOSE).append("\n");
762 rpc.append(EDIT_CONFIG_CLOSE).append("\n");
763 rpc.append(RPC_CLOSE);
764 rpc.append(ENDPATTERN);
Yuta HIGUCHIe34c9c22017-12-04 17:47:14 -0800765 log.debug("{}", rpc);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700766 String reply = sendRequest(rpc.toString());
767 return checkReply(reply);
768 }
769
770 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700771 public boolean copyConfig(DatastoreId destination,
772 DatastoreId source)
773 throws NetconfException {
774 return bareCopyConfig(destination.asXml(), source.asXml());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700775 }
776
777 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700778 public boolean copyConfig(DatastoreId netconfTargetConfig,
779 String newConfiguration)
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700780 throws NetconfException {
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700781 return bareCopyConfig(netconfTargetConfig.asXml(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200782 normalizeCopyConfigParam(newConfiguration));
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700783 }
784
785 @Override
786 public boolean copyConfig(String netconfTargetConfig,
787 String newConfiguration) throws NetconfException {
788 return bareCopyConfig(normalizeCopyConfigParam(netconfTargetConfig),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200789 normalizeCopyConfigParam(newConfiguration));
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700790 }
791
792 /**
793 * Normalize String parameter passed to copy-config API.
794 * <p>
795 * Provided for backward compatibility purpose
796 *
797 * @param input passed to copyConfig API
798 * @return XML likely to be suitable for copy-config source or target
799 */
800 private static CharSequence normalizeCopyConfigParam(String input) {
801 input = input.trim();
802 if (input.startsWith("<url")) {
803 return input;
804 } else if (!input.startsWith("<")) {
805 // assume it is a datastore name
806 return DatastoreId.datastore(input).asXml();
807 } else if (!input.startsWith("<config>")) {
808 return "<config>" + input + "</config>";
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700809 }
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700810 return input;
811 }
812
813 private boolean bareCopyConfig(CharSequence target,
814 CharSequence source)
815 throws NetconfException {
816
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700817 StringBuilder rpc = new StringBuilder(XML_HEADER);
818 rpc.append(RPC_OPEN);
819 rpc.append(NETCONF_BASE_NAMESPACE).append(">\n");
820 rpc.append("<copy-config>");
821 rpc.append("<target>");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700822 rpc.append(target);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700823 rpc.append("</target>");
824 rpc.append("<source>");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700825 rpc.append(source);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700826 rpc.append("</source>");
827 rpc.append("</copy-config>");
828 rpc.append("</rpc>");
829 rpc.append(ENDPATTERN);
830 return checkReply(sendRequest(rpc.toString()));
831 }
832
833 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700834 public boolean deleteConfig(DatastoreId netconfTargetConfig) throws NetconfException {
835 if (netconfTargetConfig.equals(DatastoreId.RUNNING)) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700836 log.warn("Target configuration for delete operation can't be \"running\"",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200837 netconfTargetConfig);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700838 return false;
839 }
840 StringBuilder rpc = new StringBuilder(XML_HEADER);
841 rpc.append("<rpc>");
842 rpc.append("<delete-config>");
843 rpc.append("<target>");
844 rpc.append("<").append(netconfTargetConfig).append("/>");
845 rpc.append("</target>");
846 rpc.append("</delete-config>");
847 rpc.append("</rpc>");
848 rpc.append(ENDPATTERN);
849 return checkReply(sendRequest(rpc.toString()));
850 }
851
852 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700853 public boolean lock(DatastoreId configType) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700854 StringBuilder rpc = new StringBuilder(XML_HEADER);
855 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
856 rpc.append("<lock>");
857 rpc.append("<target>");
858 rpc.append("<");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700859 rpc.append(configType.id());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700860 rpc.append("/>");
861 rpc.append("</target>");
862 rpc.append("</lock>");
863 rpc.append("</rpc>");
864 rpc.append(ENDPATTERN);
865 String lockReply = sendRequest(rpc.toString());
866 return checkReply(lockReply);
867 }
868
869 @Override
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700870 public boolean unlock(DatastoreId configType) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700871 StringBuilder rpc = new StringBuilder(XML_HEADER);
872 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
873 rpc.append("<unlock>");
874 rpc.append("<target>");
875 rpc.append("<");
Yuta HIGUCHI26c397c2017-05-19 12:52:28 -0700876 rpc.append(configType.id());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700877 rpc.append("/>");
878 rpc.append("</target>");
879 rpc.append("</unlock>");
880 rpc.append("</rpc>");
881 rpc.append(ENDPATTERN);
882 String unlockReply = sendRequest(rpc.toString());
883 return checkReply(unlockReply);
884 }
885
886 @Override
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700887 public boolean close() throws NetconfException {
888 return close(false);
889 }
890
891 private boolean close(boolean force) throws NetconfException {
892 StringBuilder rpc = new StringBuilder();
893 rpc.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">");
894 if (force) {
895 rpc.append("<kill-session/>");
896 } else {
897 rpc.append("<close-session/>");
898 }
899 rpc.append("</rpc>");
900 rpc.append(ENDPATTERN);
901 return checkReply(sendRequest(rpc.toString())) || close(true);
902 }
903
904 @Override
905 public String getSessionId() {
906 return sessionID;
907 }
908
909 @Override
910 public Set<String> getDeviceCapabilitiesSet() {
911 return Collections.unmodifiableSet(deviceCapabilities);
912 }
913
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700914 @Override
915 public void setOnosCapabilities(Iterable<String> capabilities) {
916 onosCapabilities = capabilities;
917 }
918
919
920 @Override
921 public void addDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
922 streamHandler.addDeviceEventListener(listener);
923 primaryListeners.add(listener);
924 }
925
926 @Override
927 public void removeDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
928 primaryListeners.remove(listener);
929 streamHandler.removeDeviceEventListener(listener);
930 }
931
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700932 private boolean checkReply(String reply) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700933 if (reply != null) {
934 if (!reply.contains("<rpc-error>")) {
935 log.debug("Device {} sent reply {}", deviceInfo, reply);
936 return true;
937 } else if (reply.contains("<ok/>")
938 || (reply.contains("<rpc-error>")
939 && reply.contains("warning"))) {
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700940 // FIXME rpc-error with a warning is considered same as Ok??
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700941 log.debug("Device {} sent reply {}", deviceInfo, reply);
942 return true;
943 }
944 }
945 log.warn("Device {} has error in reply {}", deviceInfo, reply);
946 return false;
947 }
948
Sean Condon7347de92017-07-21 12:17:25 +0100949 protected void publishEvent(NetconfDeviceOutputEvent event) {
950 primaryListeners.forEach(lsnr -> {
951 if (lsnr.isRelevant(event)) {
952 lsnr.event(event);
953 }
954 });
955 }
956
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700957 static class NotificationSession extends NetconfSessionMinaImpl {
958
959 private String notificationFilter;
960
961 NotificationSession(NetconfDeviceInfo deviceInfo)
962 throws NetconfException {
963 super(deviceInfo);
964 }
965
966 @Override
967 protected void startSubscriptionStream(String filterSchema)
968 throws NetconfException {
969
970 notificationFilter = filterSchema;
971 requestSync(createSubscriptionString(filterSchema));
972 }
973
974 @Override
975 public String toString() {
976 return MoreObjects.toStringHelper(getClass())
977 .add("deviceInfo", deviceInfo)
978 .add("sessionID", getSessionId())
979 .add("notificationFilter", notificationFilter)
980 .toString();
981 }
982 }
983
984 /**
985 * Listener attached to child session for notification streaming.
986 * <p>
987 * Forwards all notification event from child session to primary session
988 * listeners.
989 */
990 private final class NotificationForwarder
991 implements NetconfDeviceOutputEventListener {
992
993 @Override
994 public boolean isRelevant(NetconfDeviceOutputEvent event) {
995 return event.type() == Type.DEVICE_NOTIFICATION;
996 }
997
998 @Override
999 public void event(NetconfDeviceOutputEvent event) {
Sean Condon7347de92017-07-21 12:17:25 +01001000 publishEvent(event);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -07001001 }
1002 }
1003
1004 public class NetconfSessionDelegateImpl implements NetconfSessionDelegate {
1005
1006 @Override
1007 public void notify(NetconfDeviceOutputEvent event) {
1008 Optional<Integer> messageId = event.getMessageID();
1009 log.debug("messageID {}, waiting replies messageIDs {}", messageId,
Kamil Stasiak9f59f442017-05-02 11:02:24 +02001010 replies.keySet());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -07001011 if (!messageId.isPresent()) {
1012 errorReplies.add(event.getMessagePayload());
1013 log.error("Device {} sent error reply {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +02001014 event.getDeviceInfo(), event.getMessagePayload());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -07001015 return;
1016 }
1017 CompletableFuture<String> completedReply =
Sean Condon7347de92017-07-21 12:17:25 +01001018 replies.get(messageId.get()); // remove(..)?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -07001019 if (completedReply != null) {
1020 completedReply.complete(event.getMessagePayload());
1021 }
1022 }
1023 }
1024
Yuta HIGUCHI2ee4fba2018-06-12 16:21:06 -07001025 /**
1026 * @deprecated in 1.14.0
1027 */
1028 @Deprecated
Andrea Campanella7bbe7b12017-05-03 16:03:38 -07001029 public static class MinaSshNetconfSessionFactory implements NetconfSessionFactory {
1030
1031 @Override
1032 public NetconfSession createNetconfSession(NetconfDeviceInfo netconfDeviceInfo) throws NetconfException {
1033 return new NetconfSessionMinaImpl(netconfDeviceInfo);
1034 }
1035 }
Sean Condon54d82432017-07-26 22:27:25 +01001036}