blob: 48be1992fc34228b50f1f22c70d84a1819021e0c [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;
quan PHAM VAN32d70e52018-08-01 17:35:30 -070024import com.google.common.collect.Sets;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070025import org.apache.sshd.client.SshClient;
26import org.apache.sshd.client.channel.ClientChannel;
27import org.apache.sshd.client.future.ConnectFuture;
28import org.apache.sshd.client.future.OpenFuture;
29import org.apache.sshd.client.session.ClientSession;
Sean Condon7347de92017-07-21 12:17:25 +010030import org.apache.sshd.common.FactoryManager;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070031import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
Holger Schulz092cbbf2017-08-31 17:52:30 +020032import org.bouncycastle.jce.provider.BouncyCastleProvider;
Holger Schulz092cbbf2017-08-31 17:52:30 +020033import org.bouncycastle.openssl.PEMKeyPair;
quan PHAM VAN32d70e52018-08-01 17:35:30 -070034import org.bouncycastle.openssl.PEMParser;
Holger Schulz092cbbf2017-08-31 17:52:30 +020035import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
quan PHAM VAN32d70e52018-08-01 17:35:30 -070036import org.onlab.osgi.DefaultServiceDirectory;
37import org.onlab.osgi.ServiceDirectory;
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -070038import org.onlab.util.SharedExecutors;
quan PHAM VAN32d70e52018-08-01 17:35:30 -070039import org.onosproject.net.DeviceId;
40import org.onosproject.net.driver.Driver;
41import org.onosproject.net.driver.DriverService;
Yuta HIGUCHI4f55c672018-06-14 18:10:43 -070042import org.onosproject.netconf.AbstractNetconfSession;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070043import org.onosproject.netconf.NetconfDeviceInfo;
44import org.onosproject.netconf.NetconfDeviceOutputEvent;
45import org.onosproject.netconf.NetconfDeviceOutputEvent.Type;
46import org.onosproject.netconf.NetconfDeviceOutputEventListener;
47import org.onosproject.netconf.NetconfException;
48import org.onosproject.netconf.NetconfSession;
49import org.onosproject.netconf.NetconfSessionFactory;
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -070050import org.onosproject.netconf.NetconfTransportException;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070051import org.slf4j.Logger;
Yuta HIGUCHI15677982017-08-16 15:50:29 -070052
Holger Schulz092cbbf2017-08-31 17:52:30 +020053import java.io.CharArrayReader;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070054import java.io.IOException;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070055import java.security.KeyFactory;
56import java.security.KeyPair;
57import java.security.NoSuchAlgorithmException;
58import java.security.PublicKey;
59import java.security.spec.InvalidKeySpecException;
60import java.security.spec.X509EncodedKeySpec;
quan PHAM VAN32d70e52018-08-01 17:35:30 -070061import java.util.ArrayList;
62import java.util.Arrays;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070063import java.util.Collection;
64import java.util.Collections;
quan PHAM VAN32d70e52018-08-01 17:35:30 -070065import java.util.LinkedHashSet;
66import java.util.List;
67import java.util.Map;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070068import java.util.Optional;
quan PHAM VAN32d70e52018-08-01 17:35:30 -070069import java.util.Set;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070070import java.util.concurrent.CompletableFuture;
71import java.util.concurrent.ConcurrentHashMap;
72import java.util.concurrent.CopyOnWriteArrayList;
73import java.util.concurrent.ExecutionException;
74import java.util.concurrent.TimeUnit;
75import java.util.concurrent.TimeoutException;
76import java.util.concurrent.atomic.AtomicInteger;
77import java.util.regex.Matcher;
78import java.util.regex.Pattern;
79
quan PHAM VAN32d70e52018-08-01 17:35:30 -070080import static java.nio.charset.StandardCharsets.UTF_8;
81import static org.slf4j.LoggerFactory.getLogger;
82
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070083/**
84 * Implementation of a NETCONF session to talk to a device.
85 */
Yuta HIGUCHI4f55c672018-06-14 18:10:43 -070086public class NetconfSessionMinaImpl extends AbstractNetconfSession {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070087
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -070088 private static final Logger log = getLogger(NetconfSessionMinaImpl.class);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070089
Yuta HIGUCHI371667d2017-09-05 17:30:51 -070090 /**
91 * NC 1.0, RFC4742 EOM sequence.
92 */
Andrea Campanella7bbe7b12017-05-03 16:03:38 -070093 private static final String ENDPATTERN = "]]>]]>";
94 private static final String MESSAGE_ID_STRING = "message-id";
95 private static final String HELLO = "<hello";
96 private static final String NEW_LINE = "\n";
97 private static final String END_OF_RPC_OPEN_TAG = "\">";
98 private static final String EQUAL = "=";
99 private static final String NUMBER_BETWEEN_QUOTES_MATCHER = "\"+([0-9]+)+\"";
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700100 private static final String SUBTREE_FILTER_CLOSE = "</filter>";
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700101 // FIXME hard coded namespace nc
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700102 private static final String XML_HEADER =
103 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
Yuta HIGUCHI4f55c672018-06-14 18:10:43 -0700104
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700105 // FIXME hard coded namespace base10
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700106 private static final String SUBSCRIPTION_SUBTREE_FILTER_OPEN =
107 "<filter xmlns:base10=\"urn:ietf:params:xml:ns:netconf:base:1.0\" base10:type=\"subtree\">";
108
109 private static final String INTERLEAVE_CAPABILITY_STRING = "urn:ietf:params:netconf:capability:interleave:1.0";
110
111 private static final String CAPABILITY_REGEX = "<capability>\\s*(.*?)\\s*</capability>";
112 private static final Pattern CAPABILITY_REGEX_PATTERN = Pattern.compile(CAPABILITY_REGEX);
113
114 private static final String SESSION_ID_REGEX = "<session-id>\\s*(.*?)\\s*</session-id>";
115 private static final Pattern SESSION_ID_REGEX_PATTERN = Pattern.compile(SESSION_ID_REGEX);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200116 private static final String HASH = "#";
117 private static final String LF = "\n";
118 private static final String MSGLEN_REGEX_PATTERN = "\n#\\d+\n";
119 private static final String NETCONF_10_CAPABILITY = "urn:ietf:params:netconf:base:1.0";
120 private static final String NETCONF_11_CAPABILITY = "urn:ietf:params:netconf:base:1.1";
quan PHAM VAN32d70e52018-08-01 17:35:30 -0700121 private static final String NETCONF_CLIENT_CAPABILITY = "netconfClientCapability";
122
123 private static ServiceDirectory directory = new DefaultServiceDirectory();
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700124
125 private String sessionID;
126 private final AtomicInteger messageIdInteger = new AtomicInteger(1);
127 protected final NetconfDeviceInfo deviceInfo;
128 private Iterable<String> onosCapabilities =
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200129 ImmutableList.of(NETCONF_10_CAPABILITY, NETCONF_11_CAPABILITY);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700130
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700131 private final Set<String> deviceCapabilities = new LinkedHashSet<>();
132 private NetconfStreamHandler streamHandler;
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700133 // FIXME ONOS-7019 key type should be revised to a String, see RFC6241
134 /**
135 * Message-ID and corresponding Future waiting for response.
136 */
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700137 private Map<Integer, CompletableFuture<String>> replies;
Sean Condon7347de92017-07-21 12:17:25 +0100138 private List<String> errorReplies; // Not sure why we need this?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700139 private boolean subscriptionConnected = false;
140 private String notificationFilterSchema = null;
141
142 private final Collection<NetconfDeviceOutputEventListener> primaryListeners =
143 new CopyOnWriteArrayList<>();
144 private final Collection<NetconfSession> children =
145 new CopyOnWriteArrayList<>();
146
Sean Condon54d82432017-07-26 22:27:25 +0100147 private int connectTimeout;
148 private int replyTimeout;
149 private int idleTimeout;
150
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700151 private ClientChannel channel = null;
152 private ClientSession session = null;
153 private SshClient client = null;
154
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700155 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo) throws NetconfException {
156 this.deviceInfo = deviceInfo;
157 replies = new ConcurrentHashMap<>();
158 errorReplies = new ArrayList<>();
quan PHAM VAN32d70e52018-08-01 17:35:30 -0700159 Set<String> capabilities = getClientCapabilites(deviceInfo.getDeviceId());
160 if (!capabilities.isEmpty()) {
161 capabilities.addAll(Sets.newHashSet(onosCapabilities));
162 setOnosCapabilities(capabilities);
163 }
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700164 // FIXME should not immediately start session on construction
165 // setOnosCapabilities() is useless due to this behavior
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700166 startConnection();
167 }
168
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200169 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo, List<String> capabilities) throws NetconfException {
170 this.deviceInfo = deviceInfo;
171 replies = new ConcurrentHashMap<>();
172 errorReplies = new ArrayList<>();
173 setOnosCapabilities(capabilities);
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700174 // FIXME should not immediately start session on construction
175 // setOnosCapabilities() is useless due to this behavior
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200176 startConnection();
177 }
178
quan PHAM VAN32d70e52018-08-01 17:35:30 -0700179 /**
180 * Get the list of the netconf client capabilities from device driver property.
181 *
182 * @param deviceId the deviceID for which to recover the capabilities from the driver.
183 * @return the String list of clientCapability property, or null if it is not configured
184 */
185 public Set<String> getClientCapabilites(DeviceId deviceId) {
186 Set<String> capabilities = new LinkedHashSet<>();
187 DriverService driverService = directory.get(DriverService.class);
188 Driver driver = driverService.getDriver(deviceId);
189 if (driver == null) {
190 return capabilities;
191 }
192 String clientCapabilities = driver.getProperty(NETCONF_CLIENT_CAPABILITY);
193 if (clientCapabilities == null) {
194 return capabilities;
195 }
196 String[] textStr = clientCapabilities.split("\\|");
197 capabilities.addAll(Arrays.asList(textStr));
198 return capabilities;
199 }
200
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700201 private void startConnection() throws NetconfException {
Sean Condon54d82432017-07-26 22:27:25 +0100202 connectTimeout = deviceInfo.getConnectTimeoutSec().orElse(
203 NetconfControllerImpl.netconfConnectTimeout);
204 replyTimeout = deviceInfo.getReplyTimeoutSec().orElse(
205 NetconfControllerImpl.netconfReplyTimeout);
206 idleTimeout = deviceInfo.getIdleTimeoutSec().orElse(
207 NetconfControllerImpl.netconfIdleTimeout);
208 log.info("Connecting to {} with timeouts C:{}, R:{}, I:{}", deviceInfo,
209 connectTimeout, replyTimeout, idleTimeout);
210
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700211 try {
212 startClient();
213 } catch (IOException e) {
214 throw new NetconfException("Failed to establish SSH with device " + deviceInfo, e);
215 }
216 }
217
218 private void startClient() throws IOException {
Yuta HIGUCHI2ee4fba2018-06-12 16:21:06 -0700219 log.info("Creating NETCONF session to {}",
220 deviceInfo.getDeviceId());
221
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700222 client = SshClient.setUpDefaultClient();
Sean Condon7347de92017-07-21 12:17:25 +0100223 client.getProperties().putIfAbsent(FactoryManager.IDLE_TIMEOUT,
Sean Condon54d82432017-07-26 22:27:25 +0100224 TimeUnit.SECONDS.toMillis(idleTimeout));
Sean Condon7347de92017-07-21 12:17:25 +0100225 client.getProperties().putIfAbsent(FactoryManager.NIO2_READ_TIMEOUT,
Sean Condon54d82432017-07-26 22:27:25 +0100226 TimeUnit.SECONDS.toMillis(idleTimeout + 15L));
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700227 client.start();
228 client.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
229 startSession();
230 }
231
Yuta HIGUCHI4f55c672018-06-14 18:10:43 -0700232 //TODO: Remove the default methods already implemented in NetconfSession
233
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700234 // FIXME blocking
235 @Deprecated
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700236 private void startSession() throws IOException {
237 final ConnectFuture connectFuture;
238 connectFuture = client.connect(deviceInfo.name(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200239 deviceInfo.ip().toString(),
240 deviceInfo.port())
Sean Condon54d82432017-07-26 22:27:25 +0100241 .verify(connectTimeout, TimeUnit.SECONDS);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700242 session = connectFuture.getSession();
243 //Using the device ssh key if possible
244 if (deviceInfo.getKey() != null) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700245 try (PEMParser pemParser = new PEMParser(new CharArrayReader(deviceInfo.getKey()))) {
246 JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME);
247 try {
248 KeyPair kp = converter.getKeyPair((PEMKeyPair) pemParser.readObject());
249 session.addPublicKeyIdentity(kp);
250 } catch (IOException e) {
251 throw new NetconfException("Failed to authenticate session with device " +
252 deviceInfo + "check key to be a valid key", e);
253 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700254 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700255 } else {
256 session.addPasswordIdentity(deviceInfo.password());
257 }
Sean Condon54d82432017-07-26 22:27:25 +0100258 session.auth().verify(connectTimeout, TimeUnit.SECONDS);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700259 Set<ClientSession.ClientSessionEvent> event = session.waitFor(
260 ImmutableSet.of(ClientSession.ClientSessionEvent.WAIT_AUTH,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200261 ClientSession.ClientSessionEvent.CLOSED,
262 ClientSession.ClientSessionEvent.AUTHED), 0);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700263
264 if (!event.contains(ClientSession.ClientSessionEvent.AUTHED)) {
265 log.debug("Session closed {} {}", event, session.isClosed());
266 throw new NetconfException("Failed to authenticate session with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200267 deviceInfo + "check the user/pwd or key");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700268 }
269 openChannel();
270 }
271
272 private PublicKey getPublicKey(byte[] keyBytes, String type)
273 throws NoSuchAlgorithmException, InvalidKeySpecException {
274
275 X509EncodedKeySpec spec =
276 new X509EncodedKeySpec(keyBytes);
277 KeyFactory kf = KeyFactory.getInstance(type);
278 return kf.generatePublic(spec);
279 }
280
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700281 // FIXME blocking
282 @Deprecated
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700283 private void openChannel() throws IOException {
284 channel = session.createSubsystemChannel("netconf");
285 OpenFuture channelFuture = channel.open();
Sean Condon54d82432017-07-26 22:27:25 +0100286 if (channelFuture.await(connectTimeout, TimeUnit.SECONDS)) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700287 if (channelFuture.isOpened()) {
288 streamHandler = new NetconfStreamThread(channel.getInvertedOut(), channel.getInvertedIn(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200289 channel.getInvertedErr(), deviceInfo,
290 new NetconfSessionDelegateImpl(), replies);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700291 } else {
292 throw new NetconfException("Failed to open channel with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200293 deviceInfo);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700294 }
295 sendHello();
296 }
297 }
298
299
300 @Beta
301 protected void startSubscriptionStream(String filterSchema) throws NetconfException {
302 boolean openNewSession = false;
303 if (!deviceCapabilities.contains(INTERLEAVE_CAPABILITY_STRING)) {
304 log.info("Device {} doesn't support interleave, creating child session", deviceInfo);
305 openNewSession = true;
306
307 } else if (subscriptionConnected &&
308 notificationFilterSchema != null &&
309 !Objects.equal(filterSchema, notificationFilterSchema)) {
310 // interleave supported and existing filter is NOT "no filtering"
311 // and was requested with different filtering schema
312 log.info("Cannot use existing session for subscription {} ({})",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200313 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700314 openNewSession = true;
315 }
316
317 if (openNewSession) {
318 log.info("Creating notification session to {} with filter {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200319 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700320 NetconfSession child = new NotificationSession(deviceInfo);
321
322 child.addDeviceOutputListener(new NotificationForwarder());
323
324 child.startSubscription(filterSchema);
325 children.add(child);
326 return;
327 }
328
329 // request to start interleaved notification session
330 String reply = sendRequest(createSubscriptionString(filterSchema));
331 if (!checkReply(reply)) {
332 throw new NetconfException("Subscription not successful with device "
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200333 + deviceInfo + " with reply " + reply);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700334 }
335 subscriptionConnected = true;
336 }
337
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700338 @Beta
339 @Override
340 public void startSubscription(String filterSchema) throws NetconfException {
341 if (!subscriptionConnected) {
342 notificationFilterSchema = filterSchema;
343 startSubscriptionStream(filterSchema);
344 }
345 streamHandler.setEnableNotifications(true);
346 }
347
348 @Beta
349 protected String createSubscriptionString(String filterSchema) {
350 StringBuilder subscriptionbuffer = new StringBuilder();
351 subscriptionbuffer.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
352 subscriptionbuffer.append(" <create-subscription\n");
353 subscriptionbuffer.append("xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">\n");
354 // FIXME Only subtree filtering supported at the moment.
355 if (filterSchema != null) {
356 subscriptionbuffer.append(" ");
357 subscriptionbuffer.append(SUBSCRIPTION_SUBTREE_FILTER_OPEN).append(NEW_LINE);
358 subscriptionbuffer.append(filterSchema).append(NEW_LINE);
359 subscriptionbuffer.append(" ");
360 subscriptionbuffer.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
361 }
362 subscriptionbuffer.append(" </create-subscription>\n");
363 subscriptionbuffer.append("</rpc>\n");
364 subscriptionbuffer.append(ENDPATTERN);
365 return subscriptionbuffer.toString();
366 }
367
368 @Override
369 public void endSubscription() throws NetconfException {
370 if (subscriptionConnected) {
371 streamHandler.setEnableNotifications(false);
372 } else {
373 throw new NetconfException("Subscription does not exist.");
374 }
375 }
376
377 private void sendHello() throws NetconfException {
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700378 String serverHelloResponse = sendRequest(createHelloString(), true);
379 Matcher capabilityMatcher = CAPABILITY_REGEX_PATTERN.matcher(serverHelloResponse);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700380 while (capabilityMatcher.find()) {
381 deviceCapabilities.add(capabilityMatcher.group(1));
382 }
383 sessionID = String.valueOf(-1);
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700384 Matcher sessionIDMatcher = SESSION_ID_REGEX_PATTERN.matcher(serverHelloResponse);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700385 if (sessionIDMatcher.find()) {
386 sessionID = sessionIDMatcher.group(1);
387 } else {
388 throw new NetconfException("Missing SessionID in server hello " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200389 "reponse.");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700390 }
391
392 }
393
394 private String createHelloString() {
395 StringBuilder hellobuffer = new StringBuilder();
396 hellobuffer.append(XML_HEADER);
397 hellobuffer.append("\n");
398 hellobuffer.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
399 hellobuffer.append(" <capabilities>\n");
400 onosCapabilities.forEach(
401 cap -> hellobuffer.append(" <capability>")
402 .append(cap)
403 .append("</capability>\n"));
404 hellobuffer.append(" </capabilities>\n");
405 hellobuffer.append("</hello>\n");
406 hellobuffer.append(ENDPATTERN);
407 return hellobuffer.toString();
408
409 }
410
411 @Override
412 public void checkAndReestablish() throws NetconfException {
413 try {
414 if (client.isClosed()) {
415 log.debug("Trying to restart the whole SSH connection with {}", deviceInfo.getDeviceId());
416 cleanUp();
417 startConnection();
418 } else if (session.isClosed()) {
419 log.debug("Trying to restart the session with {}", session, deviceInfo.getDeviceId());
420 cleanUp();
421 startSession();
422 } else if (channel.isClosed()) {
423 log.debug("Trying to reopen the channel with {}", deviceInfo.getDeviceId());
424 cleanUp();
425 openChannel();
Andrea Campanella856f3132017-10-23 15:46:36 +0200426 } else {
427 return;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700428 }
429 if (subscriptionConnected) {
430 log.debug("Restarting subscription with {}", deviceInfo.getDeviceId());
431 subscriptionConnected = false;
432 startSubscription(notificationFilterSchema);
433 }
Sean Condon7347de92017-07-21 12:17:25 +0100434 } catch (IOException | IllegalStateException e) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700435 log.error("Can't reopen connection for device {}", e.getMessage());
436 throw new NetconfException("Cannot re-open the connection with device" + deviceInfo, e);
437 }
438 }
439
440 private void cleanUp() {
441 //makes sure everything is at a clean state.
442 replies.clear();
443 }
444
445 @Override
446 public String requestSync(String request) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700447 String reply = sendRequest(request);
Kim JeongWoo8b03bc52018-08-10 16:50:23 +0900448 if (!checkReply(reply)) {
449 throw new NetconfException("Request not successful with device "
450 + deviceInfo + " with reply " + reply);
451 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700452 return reply;
453 }
454
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200455
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700456 // FIXME rename to align with what it actually do
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200457 /**
458 * Validate and format netconf message.
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700459 * - NC1.0 if no EOM sequence present on {@code message}, append.
460 * - NC1.1 chunk-encode given message unless it already is chunk encoded
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200461 *
462 * @param message to format
463 * @return formated message
464 */
465 private String formatNetconfMessage(String message) {
466 if (deviceCapabilities.contains(NETCONF_11_CAPABILITY)) {
467 message = formatChunkedMessage(message);
468 } else {
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700469 if (!message.endsWith(ENDPATTERN)) {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200470 message = message + NEW_LINE + ENDPATTERN;
471 }
472 }
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700473 return message;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200474 }
475
476 /**
477 * Validate and format message according to chunked framing mechanism.
478 *
479 * @param message to format
480 * @return formated message
481 */
482 private String formatChunkedMessage(String message) {
483 if (message.endsWith(ENDPATTERN)) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700484 // message given had Netconf 1.0 EOM pattern -> remove
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200485 message = message.substring(0, message.length() - ENDPATTERN.length());
486 }
487 if (!message.startsWith(LF + HASH)) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700488 // chunk encode message
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700489 message = LF + HASH + message.getBytes(UTF_8).length + LF + message + LF + HASH + HASH + LF;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200490 }
491 return message;
492 }
493
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700494 @Override
495 @Deprecated
496 public CompletableFuture<String> request(String request) {
497 return streamHandler.sendMessage(request);
498 }
499
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700500 /**
501 * {@inheritDoc}
502 * <p>
503 * FIXME Note: as of 1.12.0
504 * {@code request} must not include message-id, this method will assign
505 * and insert message-id on it's own.
506 * Will require ONOS-7019 to remove this limitation.
507 */
508 @Override
509 public CompletableFuture<String> rpc(String request) {
510
511 String rpc = request;
512 // - assign message-id
513 int msgId = messageIdInteger.incrementAndGet();
514 // - re-write request to insert message-id
515 // FIXME avoid using formatRequestMessageId
516 rpc = formatRequestMessageId(rpc, msgId);
517 // - ensure it contains XML header
518 rpc = formatXmlHeader(rpc);
519 // - use chunked framing if talking to NC 1.1 device
520 // FIXME avoid using formatNetconfMessage
521 rpc = formatNetconfMessage(rpc);
522
523 // TODO session liveness check & recovery
524
525 log.debug("Sending {} to {}", rpc, this.deviceInfo.getDeviceId());
526 return streamHandler.sendMessage(rpc, msgId)
527 .handleAsync((reply, t) -> {
528 if (t != null) {
529 // secure transport-layer error
530 // cannot use NetconfException, which is
531 // checked Exception.
532 throw new NetconfTransportException(t);
533 } else {
534 // FIXME avoid using checkReply, error handling is weird
Kim JeongWoo8b03bc52018-08-10 16:50:23 +0900535 if (!checkReply(reply)) {
536 throw new NetconfTransportException("rpc-request not successful with device "
537 + deviceInfo + " with reply " + reply);
538 }
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700539 return reply;
540 }
541 }, SharedExecutors.getPoolThreadExecutor());
542 }
543
Sean Condon54d82432017-07-26 22:27:25 +0100544 @Override
545 public int timeoutConnectSec() {
546 return connectTimeout;
547 }
548
549 @Override
550 public int timeoutReplySec() {
551 return replyTimeout;
552 }
553
554 @Override
555 public int timeoutIdleSec() {
556 return idleTimeout;
557 }
558
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700559 private CompletableFuture<String> request(String request, int messageId) {
560 return streamHandler.sendMessage(request, messageId);
561 }
562
563 private String sendRequest(String request) throws NetconfException {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700564 // FIXME probably chunk-encoding too early
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200565 request = formatNetconfMessage(request);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700566 return sendRequest(request, false);
567 }
568
569 private String sendRequest(String request, boolean isHello) throws NetconfException {
570 checkAndReestablish();
571 int messageId = -1;
572 if (!isHello) {
573 messageId = messageIdInteger.getAndIncrement();
574 }
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700575 // FIXME potentially re-writing chunked encoded String?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700576 request = formatXmlHeader(request);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200577 request = formatRequestMessageId(request, messageId);
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700578 log.debug("Sending request to NETCONF with timeout {} for {}",
579 replyTimeout, deviceInfo.name());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700580 CompletableFuture<String> futureReply = request(request, messageId);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700581 String rp;
582 try {
583 rp = futureReply.get(replyTimeout, TimeUnit.SECONDS);
Sean Condon7347de92017-07-21 12:17:25 +0100584 replies.remove(messageId); // Why here???
585 } catch (InterruptedException e) {
586 Thread.currentThread().interrupt();
587 throw new NetconfException("Interrupted waiting for reply for request" + request, e);
588 } catch (TimeoutException e) {
Sean Condon54d82432017-07-26 22:27:25 +0100589 throw new NetconfException("Timed out waiting for reply for request " +
590 request + " after " + replyTimeout + " sec.", e);
Sean Condon7347de92017-07-21 12:17:25 +0100591 } catch (ExecutionException e) {
592 log.warn("Closing session {} for {} due to unexpected Error", sessionID, deviceInfo, e);
593 try {
594 session.close();
595 channel.close(); //Closes the socket which should interrupt NetconfStreamThread
596 client.close();
597 } catch (IOException ioe) {
598 log.warn("Error closing session {} on {}", sessionID, deviceInfo, ioe);
599 }
600 NetconfDeviceOutputEvent event = new NetconfDeviceOutputEvent(
601 NetconfDeviceOutputEvent.Type.SESSION_CLOSED,
602 null, "Closed due to unexpected error " + e.getCause(),
603 Optional.of(-1), deviceInfo);
604 publishEvent(event);
605 errorReplies.clear(); // move to cleanUp()?
606 cleanUp();
607
608 throw new NetconfException("Closing session " + sessionID + " for " + deviceInfo +
609 " for request " + request, e);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700610 }
611 log.debug("Result {} from request {} to device {}", rp, request, deviceInfo);
612 return rp.trim();
613 }
614
615 private String formatRequestMessageId(String request, int messageId) {
616 if (request.contains(MESSAGE_ID_STRING)) {
617 //FIXME if application provides his own counting of messages this fails that count
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700618 // FIXME assumes message-id is integer. RFC6241 allows anything as long as it is allowed in XML
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700619 request = request.replaceFirst(MESSAGE_ID_STRING + EQUAL + NUMBER_BETWEEN_QUOTES_MATCHER,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200620 MESSAGE_ID_STRING + EQUAL + "\"" + messageId + "\"");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700621 } else if (!request.contains(MESSAGE_ID_STRING) && !request.contains(HELLO)) {
622 //FIXME find out a better way to enforce the presence of message-id
623 request = request.replaceFirst(END_OF_RPC_OPEN_TAG, "\" " + MESSAGE_ID_STRING + EQUAL + "\""
624 + messageId + "\"" + ">");
625 }
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700626 request = updateRequestLength(request);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200627 return request;
628 }
629
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700630 private String updateRequestLength(String request) {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200631 if (request.contains(LF + HASH + HASH + LF)) {
632 int oldLen = Integer.parseInt(request.split(HASH)[1].split(LF)[0]);
633 String rpcWithEnding = request.substring(request.indexOf('<'));
634 String firstBlock = request.split(MSGLEN_REGEX_PATTERN)[1].split(LF + HASH + HASH + LF)[0];
635 int newLen = 0;
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700636 newLen = firstBlock.getBytes(UTF_8).length;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200637 if (oldLen != newLen) {
638 return LF + HASH + newLen + LF + rpcWithEnding;
639 }
640 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700641 return request;
642 }
643
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700644 /**
645 * Ensures xml start directive/declaration appears in the {@code request}.
646 * @param request RPC request message
647 * @return XML RPC message
648 */
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700649 private String formatXmlHeader(String request) {
Sean Condon2d647172017-09-19 12:29:13 +0100650 if (!request.contains(XML_HEADER)) {
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700651 //FIXME if application provides his own XML header of different type there is a clash
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200652 if (request.startsWith(LF + HASH)) {
653 request = request.split("<")[0] + XML_HEADER + request.substring(request.split("<")[0].length());
654 } else {
655 request = XML_HEADER + "\n" + request;
656 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700657 }
658 return request;
659 }
660
661 @Override
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700662 public String getSessionId() {
663 return sessionID;
664 }
665
666 @Override
667 public Set<String> getDeviceCapabilitiesSet() {
668 return Collections.unmodifiableSet(deviceCapabilities);
669 }
670
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700671 @Override
672 public void setOnosCapabilities(Iterable<String> capabilities) {
673 onosCapabilities = capabilities;
674 }
675
676
677 @Override
678 public void addDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
679 streamHandler.addDeviceEventListener(listener);
680 primaryListeners.add(listener);
681 }
682
683 @Override
684 public void removeDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
685 primaryListeners.remove(listener);
686 streamHandler.removeDeviceEventListener(listener);
687 }
688
Yuta HIGUCHI4f55c672018-06-14 18:10:43 -0700689 @Override
690 protected boolean checkReply(String reply) {
691 // Overridden to record error logs
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700692 if (reply != null) {
693 if (!reply.contains("<rpc-error>")) {
694 log.debug("Device {} sent reply {}", deviceInfo, reply);
695 return true;
696 } else if (reply.contains("<ok/>")
697 || (reply.contains("<rpc-error>")
698 && reply.contains("warning"))) {
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700699 // FIXME rpc-error with a warning is considered same as Ok??
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700700 log.debug("Device {} sent reply {}", deviceInfo, reply);
701 return true;
702 }
703 }
704 log.warn("Device {} has error in reply {}", deviceInfo, reply);
705 return false;
706 }
707
zhongguo zhao78eab372018-08-27 16:22:39 +0800708 @Override
709 public boolean close() throws NetconfException {
710 try {
711 return super.close();
712 } catch (IOException ioe) {
713 throw new NetconfException(ioe.getMessage());
714 } finally {
715 try {
716 session.close();
717 channel.close();
718 client.close();
719 } catch (IOException ioe) {
720 log.warn("Error closing session {} on {}", sessionID, deviceInfo, ioe);
721 }
722 }
723 }
724
Sean Condon7347de92017-07-21 12:17:25 +0100725 protected void publishEvent(NetconfDeviceOutputEvent event) {
726 primaryListeners.forEach(lsnr -> {
727 if (lsnr.isRelevant(event)) {
728 lsnr.event(event);
729 }
730 });
731 }
732
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700733 static class NotificationSession extends NetconfSessionMinaImpl {
734
735 private String notificationFilter;
736
737 NotificationSession(NetconfDeviceInfo deviceInfo)
738 throws NetconfException {
739 super(deviceInfo);
740 }
741
742 @Override
743 protected void startSubscriptionStream(String filterSchema)
744 throws NetconfException {
745
746 notificationFilter = filterSchema;
747 requestSync(createSubscriptionString(filterSchema));
748 }
749
750 @Override
751 public String toString() {
752 return MoreObjects.toStringHelper(getClass())
753 .add("deviceInfo", deviceInfo)
754 .add("sessionID", getSessionId())
755 .add("notificationFilter", notificationFilter)
756 .toString();
757 }
758 }
759
760 /**
761 * Listener attached to child session for notification streaming.
762 * <p>
763 * Forwards all notification event from child session to primary session
764 * listeners.
765 */
766 private final class NotificationForwarder
767 implements NetconfDeviceOutputEventListener {
768
769 @Override
770 public boolean isRelevant(NetconfDeviceOutputEvent event) {
771 return event.type() == Type.DEVICE_NOTIFICATION;
772 }
773
774 @Override
775 public void event(NetconfDeviceOutputEvent event) {
Sean Condon7347de92017-07-21 12:17:25 +0100776 publishEvent(event);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700777 }
778 }
779
780 public class NetconfSessionDelegateImpl implements NetconfSessionDelegate {
781
782 @Override
783 public void notify(NetconfDeviceOutputEvent event) {
784 Optional<Integer> messageId = event.getMessageID();
785 log.debug("messageID {}, waiting replies messageIDs {}", messageId,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200786 replies.keySet());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700787 if (!messageId.isPresent()) {
788 errorReplies.add(event.getMessagePayload());
789 log.error("Device {} sent error reply {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200790 event.getDeviceInfo(), event.getMessagePayload());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700791 return;
792 }
793 CompletableFuture<String> completedReply =
Sean Condon7347de92017-07-21 12:17:25 +0100794 replies.get(messageId.get()); // remove(..)?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700795 if (completedReply != null) {
796 completedReply.complete(event.getMessagePayload());
797 }
798 }
799 }
800
Yuta HIGUCHI2ee4fba2018-06-12 16:21:06 -0700801 /**
802 * @deprecated in 1.14.0
803 */
804 @Deprecated
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700805 public static class MinaSshNetconfSessionFactory implements NetconfSessionFactory {
806
807 @Override
808 public NetconfSession createNetconfSession(NetconfDeviceInfo netconfDeviceInfo) throws NetconfException {
809 return new NetconfSessionMinaImpl(netconfDeviceInfo);
810 }
811 }
Sean Condon54d82432017-07-26 22:27:25 +0100812}