blob: 2385f9433c1f2d050a9b7667572381a16a8dc2e1 [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 HIGUCHI4f55c672018-06-14 18:10:43 -070036import org.onosproject.netconf.AbstractNetconfSession;
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 */
Yuta HIGUCHI4f55c672018-06-14 18:10:43 -070078public class NetconfSessionMinaImpl extends AbstractNetconfSession {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070079
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]+)+\"";
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070092 private static final String SUBTREE_FILTER_CLOSE = "</filter>";
Yuta HIGUCHI371667d2017-09-05 17:30:51 -070093 // FIXME hard coded namespace nc
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070094 private static final String XML_HEADER =
95 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
Yuta HIGUCHI4f55c672018-06-14 18:10:43 -070096
Yuta HIGUCHI371667d2017-09-05 17:30:51 -070097 // FIXME hard coded namespace base10
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070098 private static final String SUBSCRIPTION_SUBTREE_FILTER_OPEN =
99 "<filter xmlns:base10=\"urn:ietf:params:xml:ns:netconf:base:1.0\" base10:type=\"subtree\">";
100
101 private static final String INTERLEAVE_CAPABILITY_STRING = "urn:ietf:params:netconf:capability:interleave:1.0";
102
103 private static final String CAPABILITY_REGEX = "<capability>\\s*(.*?)\\s*</capability>";
104 private static final Pattern CAPABILITY_REGEX_PATTERN = Pattern.compile(CAPABILITY_REGEX);
105
106 private static final String SESSION_ID_REGEX = "<session-id>\\s*(.*?)\\s*</session-id>";
107 private static final Pattern SESSION_ID_REGEX_PATTERN = Pattern.compile(SESSION_ID_REGEX);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200108 private static final String HASH = "#";
109 private static final String LF = "\n";
110 private static final String MSGLEN_REGEX_PATTERN = "\n#\\d+\n";
111 private static final String NETCONF_10_CAPABILITY = "urn:ietf:params:netconf:base:1.0";
112 private static final String NETCONF_11_CAPABILITY = "urn:ietf:params:netconf:base:1.1";
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700113
114 private String sessionID;
115 private final AtomicInteger messageIdInteger = new AtomicInteger(1);
116 protected final NetconfDeviceInfo deviceInfo;
117 private Iterable<String> onosCapabilities =
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200118 ImmutableList.of(NETCONF_10_CAPABILITY, NETCONF_11_CAPABILITY);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700119
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700120 private final Set<String> deviceCapabilities = new LinkedHashSet<>();
121 private NetconfStreamHandler streamHandler;
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700122 // FIXME ONOS-7019 key type should be revised to a String, see RFC6241
123 /**
124 * Message-ID and corresponding Future waiting for response.
125 */
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700126 private Map<Integer, CompletableFuture<String>> replies;
Sean Condon7347de92017-07-21 12:17:25 +0100127 private List<String> errorReplies; // Not sure why we need this?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700128 private boolean subscriptionConnected = false;
129 private String notificationFilterSchema = null;
130
131 private final Collection<NetconfDeviceOutputEventListener> primaryListeners =
132 new CopyOnWriteArrayList<>();
133 private final Collection<NetconfSession> children =
134 new CopyOnWriteArrayList<>();
135
Sean Condon54d82432017-07-26 22:27:25 +0100136 private int connectTimeout;
137 private int replyTimeout;
138 private int idleTimeout;
139
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700140
141 private ClientChannel channel = null;
142 private ClientSession session = null;
143 private SshClient client = null;
144
145
146 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo) throws NetconfException {
147 this.deviceInfo = deviceInfo;
148 replies = new ConcurrentHashMap<>();
149 errorReplies = new ArrayList<>();
Sean Condon54d82432017-07-26 22:27:25 +0100150
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700151 // FIXME should not immediately start session on construction
152 // setOnosCapabilities() is useless due to this behavior
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700153 startConnection();
154 }
155
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200156 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo, List<String> capabilities) throws NetconfException {
157 this.deviceInfo = deviceInfo;
158 replies = new ConcurrentHashMap<>();
159 errorReplies = new ArrayList<>();
160 setOnosCapabilities(capabilities);
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700161 // FIXME should not immediately start session on construction
162 // setOnosCapabilities() is useless due to this behavior
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200163 startConnection();
164 }
165
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700166 private void startConnection() throws NetconfException {
Sean Condon54d82432017-07-26 22:27:25 +0100167 connectTimeout = deviceInfo.getConnectTimeoutSec().orElse(
168 NetconfControllerImpl.netconfConnectTimeout);
169 replyTimeout = deviceInfo.getReplyTimeoutSec().orElse(
170 NetconfControllerImpl.netconfReplyTimeout);
171 idleTimeout = deviceInfo.getIdleTimeoutSec().orElse(
172 NetconfControllerImpl.netconfIdleTimeout);
173 log.info("Connecting to {} with timeouts C:{}, R:{}, I:{}", deviceInfo,
174 connectTimeout, replyTimeout, idleTimeout);
175
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700176 try {
177 startClient();
178 } catch (IOException e) {
179 throw new NetconfException("Failed to establish SSH with device " + deviceInfo, e);
180 }
181 }
182
183 private void startClient() throws IOException {
Yuta HIGUCHI2ee4fba2018-06-12 16:21:06 -0700184 log.info("Creating NETCONF session to {}",
185 deviceInfo.getDeviceId());
186
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700187 client = SshClient.setUpDefaultClient();
Sean Condon7347de92017-07-21 12:17:25 +0100188 client.getProperties().putIfAbsent(FactoryManager.IDLE_TIMEOUT,
Sean Condon54d82432017-07-26 22:27:25 +0100189 TimeUnit.SECONDS.toMillis(idleTimeout));
Sean Condon7347de92017-07-21 12:17:25 +0100190 client.getProperties().putIfAbsent(FactoryManager.NIO2_READ_TIMEOUT,
Sean Condon54d82432017-07-26 22:27:25 +0100191 TimeUnit.SECONDS.toMillis(idleTimeout + 15L));
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700192 client.start();
193 client.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
194 startSession();
195 }
196
Yuta HIGUCHI4f55c672018-06-14 18:10:43 -0700197 //TODO: Remove the default methods already implemented in NetconfSession
198
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700199 // FIXME blocking
200 @Deprecated
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700201 private void startSession() throws IOException {
202 final ConnectFuture connectFuture;
203 connectFuture = client.connect(deviceInfo.name(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200204 deviceInfo.ip().toString(),
205 deviceInfo.port())
Sean Condon54d82432017-07-26 22:27:25 +0100206 .verify(connectTimeout, TimeUnit.SECONDS);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700207 session = connectFuture.getSession();
208 //Using the device ssh key if possible
209 if (deviceInfo.getKey() != null) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700210 try (PEMParser pemParser = new PEMParser(new CharArrayReader(deviceInfo.getKey()))) {
211 JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME);
212 try {
213 KeyPair kp = converter.getKeyPair((PEMKeyPair) pemParser.readObject());
214 session.addPublicKeyIdentity(kp);
215 } catch (IOException e) {
216 throw new NetconfException("Failed to authenticate session with device " +
217 deviceInfo + "check key to be a valid key", e);
218 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700219 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700220 } else {
221 session.addPasswordIdentity(deviceInfo.password());
222 }
Sean Condon54d82432017-07-26 22:27:25 +0100223 session.auth().verify(connectTimeout, TimeUnit.SECONDS);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700224 Set<ClientSession.ClientSessionEvent> event = session.waitFor(
225 ImmutableSet.of(ClientSession.ClientSessionEvent.WAIT_AUTH,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200226 ClientSession.ClientSessionEvent.CLOSED,
227 ClientSession.ClientSessionEvent.AUTHED), 0);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700228
229 if (!event.contains(ClientSession.ClientSessionEvent.AUTHED)) {
230 log.debug("Session closed {} {}", event, session.isClosed());
231 throw new NetconfException("Failed to authenticate session with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200232 deviceInfo + "check the user/pwd or key");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700233 }
234 openChannel();
235 }
236
237 private PublicKey getPublicKey(byte[] keyBytes, String type)
238 throws NoSuchAlgorithmException, InvalidKeySpecException {
239
240 X509EncodedKeySpec spec =
241 new X509EncodedKeySpec(keyBytes);
242 KeyFactory kf = KeyFactory.getInstance(type);
243 return kf.generatePublic(spec);
244 }
245
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700246 // FIXME blocking
247 @Deprecated
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700248 private void openChannel() throws IOException {
249 channel = session.createSubsystemChannel("netconf");
250 OpenFuture channelFuture = channel.open();
Sean Condon54d82432017-07-26 22:27:25 +0100251 if (channelFuture.await(connectTimeout, TimeUnit.SECONDS)) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700252 if (channelFuture.isOpened()) {
253 streamHandler = new NetconfStreamThread(channel.getInvertedOut(), channel.getInvertedIn(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200254 channel.getInvertedErr(), deviceInfo,
255 new NetconfSessionDelegateImpl(), replies);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700256 } else {
257 throw new NetconfException("Failed to open channel with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200258 deviceInfo);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700259 }
260 sendHello();
261 }
262 }
263
264
265 @Beta
266 protected void startSubscriptionStream(String filterSchema) throws NetconfException {
267 boolean openNewSession = false;
268 if (!deviceCapabilities.contains(INTERLEAVE_CAPABILITY_STRING)) {
269 log.info("Device {} doesn't support interleave, creating child session", deviceInfo);
270 openNewSession = true;
271
272 } else if (subscriptionConnected &&
273 notificationFilterSchema != null &&
274 !Objects.equal(filterSchema, notificationFilterSchema)) {
275 // interleave supported and existing filter is NOT "no filtering"
276 // and was requested with different filtering schema
277 log.info("Cannot use existing session for subscription {} ({})",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200278 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700279 openNewSession = true;
280 }
281
282 if (openNewSession) {
283 log.info("Creating notification session to {} with filter {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200284 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700285 NetconfSession child = new NotificationSession(deviceInfo);
286
287 child.addDeviceOutputListener(new NotificationForwarder());
288
289 child.startSubscription(filterSchema);
290 children.add(child);
291 return;
292 }
293
294 // request to start interleaved notification session
295 String reply = sendRequest(createSubscriptionString(filterSchema));
296 if (!checkReply(reply)) {
297 throw new NetconfException("Subscription not successful with device "
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200298 + deviceInfo + " with reply " + reply);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700299 }
300 subscriptionConnected = true;
301 }
302
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700303 @Beta
304 @Override
305 public void startSubscription(String filterSchema) throws NetconfException {
306 if (!subscriptionConnected) {
307 notificationFilterSchema = filterSchema;
308 startSubscriptionStream(filterSchema);
309 }
310 streamHandler.setEnableNotifications(true);
311 }
312
313 @Beta
314 protected String createSubscriptionString(String filterSchema) {
315 StringBuilder subscriptionbuffer = new StringBuilder();
316 subscriptionbuffer.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
317 subscriptionbuffer.append(" <create-subscription\n");
318 subscriptionbuffer.append("xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">\n");
319 // FIXME Only subtree filtering supported at the moment.
320 if (filterSchema != null) {
321 subscriptionbuffer.append(" ");
322 subscriptionbuffer.append(SUBSCRIPTION_SUBTREE_FILTER_OPEN).append(NEW_LINE);
323 subscriptionbuffer.append(filterSchema).append(NEW_LINE);
324 subscriptionbuffer.append(" ");
325 subscriptionbuffer.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
326 }
327 subscriptionbuffer.append(" </create-subscription>\n");
328 subscriptionbuffer.append("</rpc>\n");
329 subscriptionbuffer.append(ENDPATTERN);
330 return subscriptionbuffer.toString();
331 }
332
333 @Override
334 public void endSubscription() throws NetconfException {
335 if (subscriptionConnected) {
336 streamHandler.setEnableNotifications(false);
337 } else {
338 throw new NetconfException("Subscription does not exist.");
339 }
340 }
341
342 private void sendHello() throws NetconfException {
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700343 String serverHelloResponse = sendRequest(createHelloString(), true);
344 Matcher capabilityMatcher = CAPABILITY_REGEX_PATTERN.matcher(serverHelloResponse);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700345 while (capabilityMatcher.find()) {
346 deviceCapabilities.add(capabilityMatcher.group(1));
347 }
348 sessionID = String.valueOf(-1);
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700349 Matcher sessionIDMatcher = SESSION_ID_REGEX_PATTERN.matcher(serverHelloResponse);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700350 if (sessionIDMatcher.find()) {
351 sessionID = sessionIDMatcher.group(1);
352 } else {
353 throw new NetconfException("Missing SessionID in server hello " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200354 "reponse.");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700355 }
356
357 }
358
359 private String createHelloString() {
360 StringBuilder hellobuffer = new StringBuilder();
361 hellobuffer.append(XML_HEADER);
362 hellobuffer.append("\n");
363 hellobuffer.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
364 hellobuffer.append(" <capabilities>\n");
365 onosCapabilities.forEach(
366 cap -> hellobuffer.append(" <capability>")
367 .append(cap)
368 .append("</capability>\n"));
369 hellobuffer.append(" </capabilities>\n");
370 hellobuffer.append("</hello>\n");
371 hellobuffer.append(ENDPATTERN);
372 return hellobuffer.toString();
373
374 }
375
376 @Override
377 public void checkAndReestablish() throws NetconfException {
378 try {
379 if (client.isClosed()) {
380 log.debug("Trying to restart the whole SSH connection with {}", deviceInfo.getDeviceId());
381 cleanUp();
382 startConnection();
383 } else if (session.isClosed()) {
384 log.debug("Trying to restart the session with {}", session, deviceInfo.getDeviceId());
385 cleanUp();
386 startSession();
387 } else if (channel.isClosed()) {
388 log.debug("Trying to reopen the channel with {}", deviceInfo.getDeviceId());
389 cleanUp();
390 openChannel();
Andrea Campanella856f3132017-10-23 15:46:36 +0200391 } else {
392 return;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700393 }
394 if (subscriptionConnected) {
395 log.debug("Restarting subscription with {}", deviceInfo.getDeviceId());
396 subscriptionConnected = false;
397 startSubscription(notificationFilterSchema);
398 }
Sean Condon7347de92017-07-21 12:17:25 +0100399 } catch (IOException | IllegalStateException e) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700400 log.error("Can't reopen connection for device {}", e.getMessage());
401 throw new NetconfException("Cannot re-open the connection with device" + deviceInfo, e);
402 }
403 }
404
405 private void cleanUp() {
406 //makes sure everything is at a clean state.
407 replies.clear();
408 }
409
410 @Override
411 public String requestSync(String request) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700412 String reply = sendRequest(request);
Kim JeongWoo8b03bc52018-08-10 16:50:23 +0900413 if (!checkReply(reply)) {
414 throw new NetconfException("Request not successful with device "
415 + deviceInfo + " with reply " + reply);
416 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700417 return reply;
418 }
419
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200420
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700421 // FIXME rename to align with what it actually do
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200422 /**
423 * Validate and format netconf message.
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700424 * - NC1.0 if no EOM sequence present on {@code message}, append.
425 * - NC1.1 chunk-encode given message unless it already is chunk encoded
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200426 *
427 * @param message to format
428 * @return formated message
429 */
430 private String formatNetconfMessage(String message) {
431 if (deviceCapabilities.contains(NETCONF_11_CAPABILITY)) {
432 message = formatChunkedMessage(message);
433 } else {
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700434 if (!message.endsWith(ENDPATTERN)) {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200435 message = message + NEW_LINE + ENDPATTERN;
436 }
437 }
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700438 return message;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200439 }
440
441 /**
442 * Validate and format message according to chunked framing mechanism.
443 *
444 * @param message to format
445 * @return formated message
446 */
447 private String formatChunkedMessage(String message) {
448 if (message.endsWith(ENDPATTERN)) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700449 // message given had Netconf 1.0 EOM pattern -> remove
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200450 message = message.substring(0, message.length() - ENDPATTERN.length());
451 }
452 if (!message.startsWith(LF + HASH)) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700453 // chunk encode message
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700454 message = LF + HASH + message.getBytes(UTF_8).length + LF + message + LF + HASH + HASH + LF;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200455 }
456 return message;
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
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700465 /**
466 * {@inheritDoc}
467 * <p>
468 * FIXME Note: as of 1.12.0
469 * {@code request} must not include message-id, this method will assign
470 * and insert message-id on it's own.
471 * Will require ONOS-7019 to remove this limitation.
472 */
473 @Override
474 public CompletableFuture<String> rpc(String request) {
475
476 String rpc = request;
477 // - assign message-id
478 int msgId = messageIdInteger.incrementAndGet();
479 // - re-write request to insert message-id
480 // FIXME avoid using formatRequestMessageId
481 rpc = formatRequestMessageId(rpc, msgId);
482 // - ensure it contains XML header
483 rpc = formatXmlHeader(rpc);
484 // - use chunked framing if talking to NC 1.1 device
485 // FIXME avoid using formatNetconfMessage
486 rpc = formatNetconfMessage(rpc);
487
488 // TODO session liveness check & recovery
489
490 log.debug("Sending {} to {}", rpc, this.deviceInfo.getDeviceId());
491 return streamHandler.sendMessage(rpc, msgId)
492 .handleAsync((reply, t) -> {
493 if (t != null) {
494 // secure transport-layer error
495 // cannot use NetconfException, which is
496 // checked Exception.
497 throw new NetconfTransportException(t);
498 } else {
499 // FIXME avoid using checkReply, error handling is weird
Kim JeongWoo8b03bc52018-08-10 16:50:23 +0900500 if (!checkReply(reply)) {
501 throw new NetconfTransportException("rpc-request not successful with device "
502 + deviceInfo + " with reply " + reply);
503 }
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700504 return reply;
505 }
506 }, SharedExecutors.getPoolThreadExecutor());
507 }
508
Sean Condon54d82432017-07-26 22:27:25 +0100509 @Override
510 public int timeoutConnectSec() {
511 return connectTimeout;
512 }
513
514 @Override
515 public int timeoutReplySec() {
516 return replyTimeout;
517 }
518
519 @Override
520 public int timeoutIdleSec() {
521 return idleTimeout;
522 }
523
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700524 private CompletableFuture<String> request(String request, int messageId) {
525 return streamHandler.sendMessage(request, messageId);
526 }
527
528 private String sendRequest(String request) throws NetconfException {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700529 // FIXME probably chunk-encoding too early
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200530 request = formatNetconfMessage(request);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700531 return sendRequest(request, false);
532 }
533
534 private String sendRequest(String request, boolean isHello) throws NetconfException {
535 checkAndReestablish();
536 int messageId = -1;
537 if (!isHello) {
538 messageId = messageIdInteger.getAndIncrement();
539 }
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700540 // FIXME potentially re-writing chunked encoded String?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700541 request = formatXmlHeader(request);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200542 request = formatRequestMessageId(request, messageId);
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700543 log.debug("Sending request to NETCONF with timeout {} for {}",
544 replyTimeout, deviceInfo.name());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700545 CompletableFuture<String> futureReply = request(request, messageId);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700546 String rp;
547 try {
548 rp = futureReply.get(replyTimeout, TimeUnit.SECONDS);
Sean Condon7347de92017-07-21 12:17:25 +0100549 replies.remove(messageId); // Why here???
550 } catch (InterruptedException e) {
551 Thread.currentThread().interrupt();
552 throw new NetconfException("Interrupted waiting for reply for request" + request, e);
553 } catch (TimeoutException e) {
Sean Condon54d82432017-07-26 22:27:25 +0100554 throw new NetconfException("Timed out waiting for reply for request " +
555 request + " after " + replyTimeout + " sec.", e);
Sean Condon7347de92017-07-21 12:17:25 +0100556 } catch (ExecutionException e) {
557 log.warn("Closing session {} for {} due to unexpected Error", sessionID, deviceInfo, e);
558 try {
559 session.close();
560 channel.close(); //Closes the socket which should interrupt NetconfStreamThread
561 client.close();
562 } catch (IOException ioe) {
563 log.warn("Error closing session {} on {}", sessionID, deviceInfo, ioe);
564 }
565 NetconfDeviceOutputEvent event = new NetconfDeviceOutputEvent(
566 NetconfDeviceOutputEvent.Type.SESSION_CLOSED,
567 null, "Closed due to unexpected error " + e.getCause(),
568 Optional.of(-1), deviceInfo);
569 publishEvent(event);
570 errorReplies.clear(); // move to cleanUp()?
571 cleanUp();
572
573 throw new NetconfException("Closing session " + sessionID + " for " + deviceInfo +
574 " for request " + request, e);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700575 }
576 log.debug("Result {} from request {} to device {}", rp, request, deviceInfo);
577 return rp.trim();
578 }
579
580 private String formatRequestMessageId(String request, int messageId) {
581 if (request.contains(MESSAGE_ID_STRING)) {
582 //FIXME if application provides his own counting of messages this fails that count
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700583 // FIXME assumes message-id is integer. RFC6241 allows anything as long as it is allowed in XML
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700584 request = request.replaceFirst(MESSAGE_ID_STRING + EQUAL + NUMBER_BETWEEN_QUOTES_MATCHER,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200585 MESSAGE_ID_STRING + EQUAL + "\"" + messageId + "\"");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700586 } else if (!request.contains(MESSAGE_ID_STRING) && !request.contains(HELLO)) {
587 //FIXME find out a better way to enforce the presence of message-id
588 request = request.replaceFirst(END_OF_RPC_OPEN_TAG, "\" " + MESSAGE_ID_STRING + EQUAL + "\""
589 + messageId + "\"" + ">");
590 }
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700591 request = updateRequestLength(request);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200592 return request;
593 }
594
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700595 private String updateRequestLength(String request) {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200596 if (request.contains(LF + HASH + HASH + LF)) {
597 int oldLen = Integer.parseInt(request.split(HASH)[1].split(LF)[0]);
598 String rpcWithEnding = request.substring(request.indexOf('<'));
599 String firstBlock = request.split(MSGLEN_REGEX_PATTERN)[1].split(LF + HASH + HASH + LF)[0];
600 int newLen = 0;
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700601 newLen = firstBlock.getBytes(UTF_8).length;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200602 if (oldLen != newLen) {
603 return LF + HASH + newLen + LF + rpcWithEnding;
604 }
605 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700606 return request;
607 }
608
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700609 /**
610 * Ensures xml start directive/declaration appears in the {@code request}.
611 * @param request RPC request message
612 * @return XML RPC message
613 */
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700614 private String formatXmlHeader(String request) {
Sean Condon2d647172017-09-19 12:29:13 +0100615 if (!request.contains(XML_HEADER)) {
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700616 //FIXME if application provides his own XML header of different type there is a clash
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200617 if (request.startsWith(LF + HASH)) {
618 request = request.split("<")[0] + XML_HEADER + request.substring(request.split("<")[0].length());
619 } else {
620 request = XML_HEADER + "\n" + request;
621 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700622 }
623 return request;
624 }
625
626 @Override
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700627 public String getSessionId() {
628 return sessionID;
629 }
630
631 @Override
632 public Set<String> getDeviceCapabilitiesSet() {
633 return Collections.unmodifiableSet(deviceCapabilities);
634 }
635
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700636 @Override
637 public void setOnosCapabilities(Iterable<String> capabilities) {
638 onosCapabilities = capabilities;
639 }
640
641
642 @Override
643 public void addDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
644 streamHandler.addDeviceEventListener(listener);
645 primaryListeners.add(listener);
646 }
647
648 @Override
649 public void removeDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
650 primaryListeners.remove(listener);
651 streamHandler.removeDeviceEventListener(listener);
652 }
653
Yuta HIGUCHI4f55c672018-06-14 18:10:43 -0700654 @Override
655 protected boolean checkReply(String reply) {
656 // Overridden to record error logs
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700657 if (reply != null) {
658 if (!reply.contains("<rpc-error>")) {
659 log.debug("Device {} sent reply {}", deviceInfo, reply);
660 return true;
661 } else if (reply.contains("<ok/>")
662 || (reply.contains("<rpc-error>")
663 && reply.contains("warning"))) {
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700664 // FIXME rpc-error with a warning is considered same as Ok??
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700665 log.debug("Device {} sent reply {}", deviceInfo, reply);
666 return true;
667 }
668 }
669 log.warn("Device {} has error in reply {}", deviceInfo, reply);
670 return false;
671 }
672
Sean Condon7347de92017-07-21 12:17:25 +0100673 protected void publishEvent(NetconfDeviceOutputEvent event) {
674 primaryListeners.forEach(lsnr -> {
675 if (lsnr.isRelevant(event)) {
676 lsnr.event(event);
677 }
678 });
679 }
680
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700681 static class NotificationSession extends NetconfSessionMinaImpl {
682
683 private String notificationFilter;
684
685 NotificationSession(NetconfDeviceInfo deviceInfo)
686 throws NetconfException {
687 super(deviceInfo);
688 }
689
690 @Override
691 protected void startSubscriptionStream(String filterSchema)
692 throws NetconfException {
693
694 notificationFilter = filterSchema;
695 requestSync(createSubscriptionString(filterSchema));
696 }
697
698 @Override
699 public String toString() {
700 return MoreObjects.toStringHelper(getClass())
701 .add("deviceInfo", deviceInfo)
702 .add("sessionID", getSessionId())
703 .add("notificationFilter", notificationFilter)
704 .toString();
705 }
706 }
707
708 /**
709 * Listener attached to child session for notification streaming.
710 * <p>
711 * Forwards all notification event from child session to primary session
712 * listeners.
713 */
714 private final class NotificationForwarder
715 implements NetconfDeviceOutputEventListener {
716
717 @Override
718 public boolean isRelevant(NetconfDeviceOutputEvent event) {
719 return event.type() == Type.DEVICE_NOTIFICATION;
720 }
721
722 @Override
723 public void event(NetconfDeviceOutputEvent event) {
Sean Condon7347de92017-07-21 12:17:25 +0100724 publishEvent(event);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700725 }
726 }
727
728 public class NetconfSessionDelegateImpl implements NetconfSessionDelegate {
729
730 @Override
731 public void notify(NetconfDeviceOutputEvent event) {
732 Optional<Integer> messageId = event.getMessageID();
733 log.debug("messageID {}, waiting replies messageIDs {}", messageId,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200734 replies.keySet());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700735 if (!messageId.isPresent()) {
736 errorReplies.add(event.getMessagePayload());
737 log.error("Device {} sent error reply {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200738 event.getDeviceInfo(), event.getMessagePayload());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700739 return;
740 }
741 CompletableFuture<String> completedReply =
Sean Condon7347de92017-07-21 12:17:25 +0100742 replies.get(messageId.get()); // remove(..)?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700743 if (completedReply != null) {
744 completedReply.complete(event.getMessagePayload());
745 }
746 }
747 }
748
Yuta HIGUCHI2ee4fba2018-06-12 16:21:06 -0700749 /**
750 * @deprecated in 1.14.0
751 */
752 @Deprecated
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700753 public static class MinaSshNetconfSessionFactory implements NetconfSessionFactory {
754
755 @Override
756 public NetconfSession createNetconfSession(NetconfDeviceInfo netconfDeviceInfo) throws NetconfException {
757 return new NetconfSessionMinaImpl(netconfDeviceInfo);
758 }
759 }
Sean Condon54d82432017-07-26 22:27:25 +0100760}