blob: 9a7059715539dbdaf7e4c860fe5e5de824abee70 [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";
Laszlo Pappbdb64082018-09-11 12:21:29 +0100122 private static final String NOTIFICATION_STREAM = "notificationStream";
quan PHAM VAN32d70e52018-08-01 17:35:30 -0700123
124 private static ServiceDirectory directory = new DefaultServiceDirectory();
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700125
126 private String sessionID;
127 private final AtomicInteger messageIdInteger = new AtomicInteger(1);
128 protected final NetconfDeviceInfo deviceInfo;
129 private Iterable<String> onosCapabilities =
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200130 ImmutableList.of(NETCONF_10_CAPABILITY, NETCONF_11_CAPABILITY);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700131
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700132 private final Set<String> deviceCapabilities = new LinkedHashSet<>();
133 private NetconfStreamHandler streamHandler;
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700134 // FIXME ONOS-7019 key type should be revised to a String, see RFC6241
135 /**
136 * Message-ID and corresponding Future waiting for response.
137 */
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700138 private Map<Integer, CompletableFuture<String>> replies;
Sean Condon7347de92017-07-21 12:17:25 +0100139 private List<String> errorReplies; // Not sure why we need this?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700140 private boolean subscriptionConnected = false;
141 private String notificationFilterSchema = null;
142
143 private final Collection<NetconfDeviceOutputEventListener> primaryListeners =
144 new CopyOnWriteArrayList<>();
145 private final Collection<NetconfSession> children =
146 new CopyOnWriteArrayList<>();
147
Sean Condon54d82432017-07-26 22:27:25 +0100148 private int connectTimeout;
149 private int replyTimeout;
150 private int idleTimeout;
151
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700152 private ClientChannel channel = null;
153 private ClientSession session = null;
154 private SshClient client = null;
155
DongRyeol Chac29f9072018-11-06 14:05:56 +0900156 private boolean disconnected = false;
157
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700158 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo) throws NetconfException {
159 this.deviceInfo = deviceInfo;
160 replies = new ConcurrentHashMap<>();
161 errorReplies = new ArrayList<>();
quan PHAM VAN32d70e52018-08-01 17:35:30 -0700162 Set<String> capabilities = getClientCapabilites(deviceInfo.getDeviceId());
163 if (!capabilities.isEmpty()) {
164 capabilities.addAll(Sets.newHashSet(onosCapabilities));
165 setOnosCapabilities(capabilities);
166 }
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700167 // FIXME should not immediately start session on construction
168 // setOnosCapabilities() is useless due to this behavior
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700169 startConnection();
170 }
171
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200172 public NetconfSessionMinaImpl(NetconfDeviceInfo deviceInfo, List<String> capabilities) throws NetconfException {
173 this.deviceInfo = deviceInfo;
174 replies = new ConcurrentHashMap<>();
175 errorReplies = new ArrayList<>();
176 setOnosCapabilities(capabilities);
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700177 // FIXME should not immediately start session on construction
178 // setOnosCapabilities() is useless due to this behavior
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200179 startConnection();
180 }
181
quan PHAM VAN32d70e52018-08-01 17:35:30 -0700182 /**
183 * Get the list of the netconf client capabilities from device driver property.
184 *
185 * @param deviceId the deviceID for which to recover the capabilities from the driver.
186 * @return the String list of clientCapability property, or null if it is not configured
187 */
188 public Set<String> getClientCapabilites(DeviceId deviceId) {
189 Set<String> capabilities = new LinkedHashSet<>();
190 DriverService driverService = directory.get(DriverService.class);
191 Driver driver = driverService.getDriver(deviceId);
192 if (driver == null) {
193 return capabilities;
194 }
195 String clientCapabilities = driver.getProperty(NETCONF_CLIENT_CAPABILITY);
196 if (clientCapabilities == null) {
197 return capabilities;
198 }
199 String[] textStr = clientCapabilities.split("\\|");
200 capabilities.addAll(Arrays.asList(textStr));
201 return capabilities;
202 }
203
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700204 private void startConnection() throws NetconfException {
Sean Condon54d82432017-07-26 22:27:25 +0100205 connectTimeout = deviceInfo.getConnectTimeoutSec().orElse(
206 NetconfControllerImpl.netconfConnectTimeout);
207 replyTimeout = deviceInfo.getReplyTimeoutSec().orElse(
208 NetconfControllerImpl.netconfReplyTimeout);
209 idleTimeout = deviceInfo.getIdleTimeoutSec().orElse(
210 NetconfControllerImpl.netconfIdleTimeout);
211 log.info("Connecting to {} with timeouts C:{}, R:{}, I:{}", deviceInfo,
212 connectTimeout, replyTimeout, idleTimeout);
213
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700214 try {
215 startClient();
DongRyeol Chac29f9072018-11-06 14:05:56 +0900216 } catch (Exception e) {
217 stopClient();
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700218 throw new NetconfException("Failed to establish SSH with device " + deviceInfo, e);
219 }
220 }
221
222 private void startClient() throws IOException {
Yuta HIGUCHI2ee4fba2018-06-12 16:21:06 -0700223 log.info("Creating NETCONF session to {}",
224 deviceInfo.getDeviceId());
225
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700226 client = SshClient.setUpDefaultClient();
Andrea Campanella59227c32019-01-07 14:52:35 +0100227 if (idleTimeout != NetconfControllerImpl.netconfIdleTimeout) {
228 client.getProperties().putIfAbsent(FactoryManager.IDLE_TIMEOUT,
229 TimeUnit.SECONDS.toMillis(idleTimeout));
230 client.getProperties().putIfAbsent(FactoryManager.NIO2_READ_TIMEOUT,
231 TimeUnit.SECONDS.toMillis(idleTimeout + 15L));
232 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700233 client.start();
234 client.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
235 startSession();
DongRyeol Chac29f9072018-11-06 14:05:56 +0900236
237 disconnected = false;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700238 }
239
Yuta HIGUCHI4f55c672018-06-14 18:10:43 -0700240 //TODO: Remove the default methods already implemented in NetconfSession
241
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700242 // FIXME blocking
243 @Deprecated
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700244 private void startSession() throws IOException {
245 final ConnectFuture connectFuture;
246 connectFuture = client.connect(deviceInfo.name(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200247 deviceInfo.ip().toString(),
248 deviceInfo.port())
Sean Condon54d82432017-07-26 22:27:25 +0100249 .verify(connectTimeout, TimeUnit.SECONDS);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700250 session = connectFuture.getSession();
251 //Using the device ssh key if possible
252 if (deviceInfo.getKey() != null) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700253 try (PEMParser pemParser = new PEMParser(new CharArrayReader(deviceInfo.getKey()))) {
254 JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME);
255 try {
256 KeyPair kp = converter.getKeyPair((PEMKeyPair) pemParser.readObject());
257 session.addPublicKeyIdentity(kp);
258 } catch (IOException e) {
259 throw new NetconfException("Failed to authenticate session with device " +
260 deviceInfo + "check key to be a valid key", e);
261 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700262 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700263 } else {
264 session.addPasswordIdentity(deviceInfo.password());
265 }
Sean Condon54d82432017-07-26 22:27:25 +0100266 session.auth().verify(connectTimeout, TimeUnit.SECONDS);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700267 Set<ClientSession.ClientSessionEvent> event = session.waitFor(
268 ImmutableSet.of(ClientSession.ClientSessionEvent.WAIT_AUTH,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200269 ClientSession.ClientSessionEvent.CLOSED,
270 ClientSession.ClientSessionEvent.AUTHED), 0);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700271
272 if (!event.contains(ClientSession.ClientSessionEvent.AUTHED)) {
273 log.debug("Session closed {} {}", event, session.isClosed());
274 throw new NetconfException("Failed to authenticate session with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200275 deviceInfo + "check the user/pwd or key");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700276 }
277 openChannel();
278 }
279
280 private PublicKey getPublicKey(byte[] keyBytes, String type)
281 throws NoSuchAlgorithmException, InvalidKeySpecException {
282
283 X509EncodedKeySpec spec =
284 new X509EncodedKeySpec(keyBytes);
285 KeyFactory kf = KeyFactory.getInstance(type);
286 return kf.generatePublic(spec);
287 }
288
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700289 // FIXME blocking
290 @Deprecated
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700291 private void openChannel() throws IOException {
292 channel = session.createSubsystemChannel("netconf");
293 OpenFuture channelFuture = channel.open();
Sean Condon54d82432017-07-26 22:27:25 +0100294 if (channelFuture.await(connectTimeout, TimeUnit.SECONDS)) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700295 if (channelFuture.isOpened()) {
296 streamHandler = new NetconfStreamThread(channel.getInvertedOut(), channel.getInvertedIn(),
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200297 channel.getInvertedErr(), deviceInfo,
298 new NetconfSessionDelegateImpl(), replies);
Andrea Campanella59227c32019-01-07 14:52:35 +0100299 primaryListeners.forEach(l -> streamHandler.addDeviceEventListener(l));
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700300 } else {
301 throw new NetconfException("Failed to open channel with device " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200302 deviceInfo);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700303 }
304 sendHello();
305 }
306 }
307
308
309 @Beta
310 protected void startSubscriptionStream(String filterSchema) throws NetconfException {
311 boolean openNewSession = false;
312 if (!deviceCapabilities.contains(INTERLEAVE_CAPABILITY_STRING)) {
313 log.info("Device {} doesn't support interleave, creating child session", deviceInfo);
314 openNewSession = true;
315
316 } else if (subscriptionConnected &&
317 notificationFilterSchema != null &&
318 !Objects.equal(filterSchema, notificationFilterSchema)) {
319 // interleave supported and existing filter is NOT "no filtering"
320 // and was requested with different filtering schema
321 log.info("Cannot use existing session for subscription {} ({})",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200322 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700323 openNewSession = true;
324 }
325
326 if (openNewSession) {
327 log.info("Creating notification session to {} with filter {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200328 deviceInfo, filterSchema);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700329 NetconfSession child = new NotificationSession(deviceInfo);
330
331 child.addDeviceOutputListener(new NotificationForwarder());
332
333 child.startSubscription(filterSchema);
334 children.add(child);
335 return;
336 }
337
338 // request to start interleaved notification session
339 String reply = sendRequest(createSubscriptionString(filterSchema));
340 if (!checkReply(reply)) {
341 throw new NetconfException("Subscription not successful with device "
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200342 + deviceInfo + " with reply " + reply);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700343 }
344 subscriptionConnected = true;
345 }
346
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700347 @Beta
348 @Override
349 public void startSubscription(String filterSchema) throws NetconfException {
350 if (!subscriptionConnected) {
351 notificationFilterSchema = filterSchema;
352 startSubscriptionStream(filterSchema);
353 }
354 streamHandler.setEnableNotifications(true);
355 }
356
357 @Beta
358 protected String createSubscriptionString(String filterSchema) {
359 StringBuilder subscriptionbuffer = new StringBuilder();
360 subscriptionbuffer.append("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
361 subscriptionbuffer.append(" <create-subscription\n");
362 subscriptionbuffer.append("xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">\n");
Laszlo Pappbdb64082018-09-11 12:21:29 +0100363 DriverService driverService = directory.get(DriverService.class);
364 Driver driver = driverService.getDriver(deviceInfo.getDeviceId());
365 if (driver != null) {
366 String stream = driver.getProperty(NOTIFICATION_STREAM);
367 if (stream != null) {
368 subscriptionbuffer.append(" <stream>");
369 subscriptionbuffer.append(stream);
370 subscriptionbuffer.append("</stream>\n");
371 }
372 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700373 // FIXME Only subtree filtering supported at the moment.
374 if (filterSchema != null) {
375 subscriptionbuffer.append(" ");
376 subscriptionbuffer.append(SUBSCRIPTION_SUBTREE_FILTER_OPEN).append(NEW_LINE);
377 subscriptionbuffer.append(filterSchema).append(NEW_LINE);
378 subscriptionbuffer.append(" ");
379 subscriptionbuffer.append(SUBTREE_FILTER_CLOSE).append(NEW_LINE);
380 }
381 subscriptionbuffer.append(" </create-subscription>\n");
382 subscriptionbuffer.append("</rpc>\n");
383 subscriptionbuffer.append(ENDPATTERN);
384 return subscriptionbuffer.toString();
385 }
386
387 @Override
388 public void endSubscription() throws NetconfException {
389 if (subscriptionConnected) {
390 streamHandler.setEnableNotifications(false);
391 } else {
392 throw new NetconfException("Subscription does not exist.");
393 }
394 }
395
DongRyeol Chac29f9072018-11-06 14:05:56 +0900396 private void stopClient() {
397 if (session != null) {
398 try {
399 session.close();
400 } catch (IOException ex) {
401 log.warn("Cannot close session {} {}", sessionID, deviceInfo, ex);
402 }
403 }
404
405 if (channel != null) {
406 try {
407 channel.close();
408 } catch (IOException ex) {
409 log.warn("Cannot close channel {} {}", sessionID, deviceInfo, ex);
410 }
411 }
412
413 if (client != null) {
414 try {
415 client.close();
416 } catch (IOException ex) {
417 log.warn("Cannot close client {} {}", sessionID, deviceInfo, ex);
418 }
419
420 client.stop();
421 }
422 }
423
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700424 private void sendHello() throws NetconfException {
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700425 String serverHelloResponse = sendRequest(createHelloString(), true);
426 Matcher capabilityMatcher = CAPABILITY_REGEX_PATTERN.matcher(serverHelloResponse);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700427 while (capabilityMatcher.find()) {
428 deviceCapabilities.add(capabilityMatcher.group(1));
429 }
430 sessionID = String.valueOf(-1);
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700431 Matcher sessionIDMatcher = SESSION_ID_REGEX_PATTERN.matcher(serverHelloResponse);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700432 if (sessionIDMatcher.find()) {
433 sessionID = sessionIDMatcher.group(1);
434 } else {
435 throw new NetconfException("Missing SessionID in server hello " +
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200436 "reponse.");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700437 }
438
439 }
440
441 private String createHelloString() {
442 StringBuilder hellobuffer = new StringBuilder();
443 hellobuffer.append(XML_HEADER);
444 hellobuffer.append("\n");
445 hellobuffer.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
446 hellobuffer.append(" <capabilities>\n");
447 onosCapabilities.forEach(
448 cap -> hellobuffer.append(" <capability>")
449 .append(cap)
450 .append("</capability>\n"));
451 hellobuffer.append(" </capabilities>\n");
452 hellobuffer.append("</hello>\n");
453 hellobuffer.append(ENDPATTERN);
454 return hellobuffer.toString();
455
456 }
457
458 @Override
459 public void checkAndReestablish() throws NetconfException {
DongRyeol Chac29f9072018-11-06 14:05:56 +0900460 if (disconnected) {
461 log.warn("Can't reopen connection for device because of disconnected {}", deviceInfo.getDeviceId());
462 throw new NetconfException("Can't reopen connection for device because of disconnected " + deviceInfo);
463 }
464
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700465 try {
zhongguo zhao98bb37a2018-08-28 16:17:06 +0800466 if (client.isClosed() || client.isClosing()) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700467 log.debug("Trying to restart the whole SSH connection with {}", deviceInfo.getDeviceId());
468 cleanUp();
469 startConnection();
zhongguo zhao98bb37a2018-08-28 16:17:06 +0800470 } else if (session.isClosed() || session.isClosing()) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700471 log.debug("Trying to restart the session with {}", session, deviceInfo.getDeviceId());
472 cleanUp();
473 startSession();
zhongguo zhao98bb37a2018-08-28 16:17:06 +0800474 } else if (channel.isClosed() || channel.isClosing()) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700475 log.debug("Trying to reopen the channel with {}", deviceInfo.getDeviceId());
476 cleanUp();
477 openChannel();
Andrea Campanella856f3132017-10-23 15:46:36 +0200478 } else {
479 return;
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700480 }
481 if (subscriptionConnected) {
482 log.debug("Restarting subscription with {}", deviceInfo.getDeviceId());
483 subscriptionConnected = false;
484 startSubscription(notificationFilterSchema);
485 }
Sean Condon7347de92017-07-21 12:17:25 +0100486 } catch (IOException | IllegalStateException e) {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700487 log.error("Can't reopen connection for device {}", e.getMessage());
488 throw new NetconfException("Cannot re-open the connection with device" + deviceInfo, e);
489 }
490 }
491
492 private void cleanUp() {
493 //makes sure everything is at a clean state.
494 replies.clear();
495 }
496
497 @Override
498 public String requestSync(String request) throws NetconfException {
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700499 String reply = sendRequest(request);
Kim JeongWoo8b03bc52018-08-10 16:50:23 +0900500 if (!checkReply(reply)) {
501 throw new NetconfException("Request not successful with device "
502 + deviceInfo + " with reply " + reply);
503 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700504 return reply;
505 }
506
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200507
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700508 // FIXME rename to align with what it actually do
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200509 /**
510 * Validate and format netconf message.
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700511 * - NC1.0 if no EOM sequence present on {@code message}, append.
512 * - NC1.1 chunk-encode given message unless it already is chunk encoded
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200513 *
514 * @param message to format
515 * @return formated message
516 */
517 private String formatNetconfMessage(String message) {
518 if (deviceCapabilities.contains(NETCONF_11_CAPABILITY)) {
519 message = formatChunkedMessage(message);
520 } else {
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700521 if (!message.endsWith(ENDPATTERN)) {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200522 message = message + NEW_LINE + ENDPATTERN;
523 }
524 }
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700525 return message;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200526 }
527
528 /**
529 * Validate and format message according to chunked framing mechanism.
530 *
531 * @param message to format
532 * @return formated message
533 */
534 private String formatChunkedMessage(String message) {
535 if (message.endsWith(ENDPATTERN)) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700536 // message given had Netconf 1.0 EOM pattern -> remove
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200537 message = message.substring(0, message.length() - ENDPATTERN.length());
538 }
539 if (!message.startsWith(LF + HASH)) {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700540 // chunk encode message
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700541 message = LF + HASH + message.getBytes(UTF_8).length + LF + message + LF + HASH + HASH + LF;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200542 }
543 return message;
544 }
545
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700546 @Override
547 @Deprecated
548 public CompletableFuture<String> request(String request) {
549 return streamHandler.sendMessage(request);
550 }
551
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700552 /**
553 * {@inheritDoc}
554 * <p>
555 * FIXME Note: as of 1.12.0
556 * {@code request} must not include message-id, this method will assign
557 * and insert message-id on it's own.
558 * Will require ONOS-7019 to remove this limitation.
559 */
560 @Override
561 public CompletableFuture<String> rpc(String request) {
562
563 String rpc = request;
564 // - assign message-id
565 int msgId = messageIdInteger.incrementAndGet();
566 // - re-write request to insert message-id
567 // FIXME avoid using formatRequestMessageId
568 rpc = formatRequestMessageId(rpc, msgId);
569 // - ensure it contains XML header
570 rpc = formatXmlHeader(rpc);
571 // - use chunked framing if talking to NC 1.1 device
572 // FIXME avoid using formatNetconfMessage
573 rpc = formatNetconfMessage(rpc);
574
575 // TODO session liveness check & recovery
576
577 log.debug("Sending {} to {}", rpc, this.deviceInfo.getDeviceId());
578 return streamHandler.sendMessage(rpc, msgId)
579 .handleAsync((reply, t) -> {
580 if (t != null) {
581 // secure transport-layer error
582 // cannot use NetconfException, which is
583 // checked Exception.
584 throw new NetconfTransportException(t);
585 } else {
586 // FIXME avoid using checkReply, error handling is weird
Kim JeongWoo8b03bc52018-08-10 16:50:23 +0900587 if (!checkReply(reply)) {
588 throw new NetconfTransportException("rpc-request not successful with device "
589 + deviceInfo + " with reply " + reply);
590 }
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700591 return reply;
592 }
593 }, SharedExecutors.getPoolThreadExecutor());
594 }
595
Sean Condon54d82432017-07-26 22:27:25 +0100596 @Override
597 public int timeoutConnectSec() {
598 return connectTimeout;
599 }
600
601 @Override
602 public int timeoutReplySec() {
603 return replyTimeout;
604 }
605
606 @Override
607 public int timeoutIdleSec() {
608 return idleTimeout;
609 }
610
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700611 private CompletableFuture<String> request(String request, int messageId) {
612 return streamHandler.sendMessage(request, messageId);
613 }
614
615 private String sendRequest(String request) throws NetconfException {
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700616 // FIXME probably chunk-encoding too early
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200617 request = formatNetconfMessage(request);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700618 return sendRequest(request, false);
619 }
620
621 private String sendRequest(String request, boolean isHello) throws NetconfException {
622 checkAndReestablish();
623 int messageId = -1;
624 if (!isHello) {
625 messageId = messageIdInteger.getAndIncrement();
626 }
Yuta HIGUCHIb2d05242017-09-05 15:44:34 -0700627 // FIXME potentially re-writing chunked encoded String?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700628 request = formatXmlHeader(request);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200629 request = formatRequestMessageId(request, messageId);
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700630 log.debug("Sending request to NETCONF with timeout {} for {}",
631 replyTimeout, deviceInfo.name());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700632 CompletableFuture<String> futureReply = request(request, messageId);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700633 String rp;
634 try {
635 rp = futureReply.get(replyTimeout, TimeUnit.SECONDS);
Sean Condon7347de92017-07-21 12:17:25 +0100636 replies.remove(messageId); // Why here???
637 } catch (InterruptedException e) {
638 Thread.currentThread().interrupt();
639 throw new NetconfException("Interrupted waiting for reply for request" + request, e);
640 } catch (TimeoutException e) {
Sean Condon54d82432017-07-26 22:27:25 +0100641 throw new NetconfException("Timed out waiting for reply for request " +
642 request + " after " + replyTimeout + " sec.", e);
Sean Condon7347de92017-07-21 12:17:25 +0100643 } catch (ExecutionException e) {
644 log.warn("Closing session {} for {} due to unexpected Error", sessionID, deviceInfo, e);
DongRyeol Chac29f9072018-11-06 14:05:56 +0900645 stopClient();
Sean Condon7347de92017-07-21 12:17:25 +0100646 NetconfDeviceOutputEvent event = new NetconfDeviceOutputEvent(
647 NetconfDeviceOutputEvent.Type.SESSION_CLOSED,
648 null, "Closed due to unexpected error " + e.getCause(),
649 Optional.of(-1), deviceInfo);
650 publishEvent(event);
651 errorReplies.clear(); // move to cleanUp()?
652 cleanUp();
653
654 throw new NetconfException("Closing session " + sessionID + " for " + deviceInfo +
655 " for request " + request, e);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700656 }
657 log.debug("Result {} from request {} to device {}", rp, request, deviceInfo);
658 return rp.trim();
659 }
660
661 private String formatRequestMessageId(String request, int messageId) {
662 if (request.contains(MESSAGE_ID_STRING)) {
663 //FIXME if application provides his own counting of messages this fails that count
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700664 // FIXME assumes message-id is integer. RFC6241 allows anything as long as it is allowed in XML
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700665 request = request.replaceFirst(MESSAGE_ID_STRING + EQUAL + NUMBER_BETWEEN_QUOTES_MATCHER,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200666 MESSAGE_ID_STRING + EQUAL + "\"" + messageId + "\"");
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700667 } else if (!request.contains(MESSAGE_ID_STRING) && !request.contains(HELLO)) {
668 //FIXME find out a better way to enforce the presence of message-id
669 request = request.replaceFirst(END_OF_RPC_OPEN_TAG, "\" " + MESSAGE_ID_STRING + EQUAL + "\""
670 + messageId + "\"" + ">");
671 }
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700672 request = updateRequestLength(request);
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200673 return request;
674 }
675
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700676 private String updateRequestLength(String request) {
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200677 if (request.contains(LF + HASH + HASH + LF)) {
678 int oldLen = Integer.parseInt(request.split(HASH)[1].split(LF)[0]);
679 String rpcWithEnding = request.substring(request.indexOf('<'));
680 String firstBlock = request.split(MSGLEN_REGEX_PATTERN)[1].split(LF + HASH + HASH + LF)[0];
681 int newLen = 0;
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700682 newLen = firstBlock.getBytes(UTF_8).length;
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200683 if (oldLen != newLen) {
684 return LF + HASH + newLen + LF + rpcWithEnding;
685 }
686 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700687 return request;
688 }
689
Yuta HIGUCHI371667d2017-09-05 17:30:51 -0700690 /**
691 * Ensures xml start directive/declaration appears in the {@code request}.
692 * @param request RPC request message
693 * @return XML RPC message
694 */
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700695 private String formatXmlHeader(String request) {
Sean Condon2d647172017-09-19 12:29:13 +0100696 if (!request.contains(XML_HEADER)) {
Yuta HIGUCHI15677982017-08-16 15:50:29 -0700697 //FIXME if application provides his own XML header of different type there is a clash
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200698 if (request.startsWith(LF + HASH)) {
699 request = request.split("<")[0] + XML_HEADER + request.substring(request.split("<")[0].length());
700 } else {
701 request = XML_HEADER + "\n" + request;
702 }
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700703 }
704 return request;
705 }
706
707 @Override
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700708 public String getSessionId() {
709 return sessionID;
710 }
711
712 @Override
713 public Set<String> getDeviceCapabilitiesSet() {
714 return Collections.unmodifiableSet(deviceCapabilities);
715 }
716
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700717 @Override
718 public void setOnosCapabilities(Iterable<String> capabilities) {
719 onosCapabilities = capabilities;
720 }
721
722
723 @Override
724 public void addDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
725 streamHandler.addDeviceEventListener(listener);
726 primaryListeners.add(listener);
727 }
728
729 @Override
730 public void removeDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
731 primaryListeners.remove(listener);
732 streamHandler.removeDeviceEventListener(listener);
733 }
734
Yuta HIGUCHI4f55c672018-06-14 18:10:43 -0700735 @Override
736 protected boolean checkReply(String reply) {
737 // Overridden to record error logs
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700738 if (reply != null) {
739 if (!reply.contains("<rpc-error>")) {
740 log.debug("Device {} sent reply {}", deviceInfo, reply);
741 return true;
742 } else if (reply.contains("<ok/>")
743 || (reply.contains("<rpc-error>")
744 && reply.contains("warning"))) {
Yuta HIGUCHI6e6c26e2017-09-06 14:25:57 -0700745 // FIXME rpc-error with a warning is considered same as Ok??
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700746 log.debug("Device {} sent reply {}", deviceInfo, reply);
747 return true;
748 }
749 }
750 log.warn("Device {} has error in reply {}", deviceInfo, reply);
751 return false;
752 }
753
zhongguo zhao78eab372018-08-27 16:22:39 +0800754 @Override
755 public boolean close() throws NetconfException {
756 try {
DongRyeol Chac29f9072018-11-06 14:05:56 +0900757 if (client != null && (client.isClosed() || client.isClosing())) {
758 return true;
759 }
760
zhongguo zhao78eab372018-08-27 16:22:39 +0800761 return super.close();
762 } catch (IOException ioe) {
763 throw new NetconfException(ioe.getMessage());
764 } finally {
DongRyeol Chac29f9072018-11-06 14:05:56 +0900765 disconnected = true;
766 stopClient();
zhongguo zhao78eab372018-08-27 16:22:39 +0800767 }
768 }
769
Sean Condon7347de92017-07-21 12:17:25 +0100770 protected void publishEvent(NetconfDeviceOutputEvent event) {
771 primaryListeners.forEach(lsnr -> {
772 if (lsnr.isRelevant(event)) {
773 lsnr.event(event);
774 }
775 });
776 }
777
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700778 static class NotificationSession extends NetconfSessionMinaImpl {
779
780 private String notificationFilter;
781
782 NotificationSession(NetconfDeviceInfo deviceInfo)
783 throws NetconfException {
784 super(deviceInfo);
785 }
786
787 @Override
788 protected void startSubscriptionStream(String filterSchema)
789 throws NetconfException {
790
791 notificationFilter = filterSchema;
792 requestSync(createSubscriptionString(filterSchema));
793 }
794
795 @Override
796 public String toString() {
797 return MoreObjects.toStringHelper(getClass())
798 .add("deviceInfo", deviceInfo)
799 .add("sessionID", getSessionId())
800 .add("notificationFilter", notificationFilter)
801 .toString();
802 }
803 }
804
805 /**
806 * Listener attached to child session for notification streaming.
807 * <p>
808 * Forwards all notification event from child session to primary session
809 * listeners.
810 */
811 private final class NotificationForwarder
812 implements NetconfDeviceOutputEventListener {
813
814 @Override
815 public boolean isRelevant(NetconfDeviceOutputEvent event) {
816 return event.type() == Type.DEVICE_NOTIFICATION;
817 }
818
819 @Override
820 public void event(NetconfDeviceOutputEvent event) {
Sean Condon7347de92017-07-21 12:17:25 +0100821 publishEvent(event);
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700822 }
823 }
824
825 public class NetconfSessionDelegateImpl implements NetconfSessionDelegate {
826
827 @Override
828 public void notify(NetconfDeviceOutputEvent event) {
829 Optional<Integer> messageId = event.getMessageID();
830 log.debug("messageID {}, waiting replies messageIDs {}", messageId,
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200831 replies.keySet());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700832 if (!messageId.isPresent()) {
833 errorReplies.add(event.getMessagePayload());
834 log.error("Device {} sent error reply {}",
Kamil Stasiak9f59f442017-05-02 11:02:24 +0200835 event.getDeviceInfo(), event.getMessagePayload());
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700836 return;
837 }
838 CompletableFuture<String> completedReply =
Sean Condon7347de92017-07-21 12:17:25 +0100839 replies.get(messageId.get()); // remove(..)?
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700840 if (completedReply != null) {
841 completedReply.complete(event.getMessagePayload());
842 }
843 }
844 }
845
Yuta HIGUCHI2ee4fba2018-06-12 16:21:06 -0700846 /**
847 * @deprecated in 1.14.0
848 */
849 @Deprecated
Andrea Campanella7bbe7b12017-05-03 16:03:38 -0700850 public static class MinaSshNetconfSessionFactory implements NetconfSessionFactory {
851
852 @Override
853 public NetconfSession createNetconfSession(NetconfDeviceInfo netconfDeviceInfo) throws NetconfException {
854 return new NetconfSessionMinaImpl(netconfDeviceInfo);
855 }
856 }
Sean Condon54d82432017-07-26 22:27:25 +0100857}