Deprecate NetconfHelloMessageAdditionalHeader 90/113090/11
authorRobert Varga <robert.varga@pantheon.tech>
Sun, 11 Aug 2024 02:17:38 +0000 (04:17 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Wed, 28 Aug 2024 19:11:35 +0000 (21:11 +0200)
This class should not exist, but getting rid of it requires us to do
quite some work, which in turn has impacts in netconf-{codec,netty-util}
and perhaps somewhere else as well.

Deprecate this class for removal to drive its replacements. Also provide
a lot of FIXMEs explaining the various changes to
netconf-{api,codec,client,server} that need to happen to solidify the
pipeline layout, the fool-proof the end user APIs and provide reliable
operation.

Change-Id: I8ec055dc6f032df3171cd0dddc78f33498956fde
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/AbstractChannelInitializer.java
netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/AbstractNetconfSession.java
netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/HelloXMLMessageDecoder.java
protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/NetconfSession.java
protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/messages/NetconfHelloMessageAdditionalHeader.java
protocol/netconf-codec/src/main/java/org/opendaylight/netconf/codec/FrameDecoder.java
protocol/netconf-codec/src/main/java/org/opendaylight/netconf/codec/MessageDecoder.java
protocol/netconf-codec/src/main/java/org/opendaylight/netconf/codec/MessageEncoder.java
protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfServerSession.java

index 56753acf61a86f7063386ead7dafacdfd9c9e8a9..17db5e177f33696e7722bf41fb949de3a5b1420d 100644 (file)
@@ -17,9 +17,18 @@ import org.opendaylight.netconf.codec.MessageDecoder;
 import org.opendaylight.netconf.codec.MessageEncoder;
 import org.opendaylight.netconf.nettyutil.handler.HelloXMLMessageDecoder;
 
+// FIXME: This is not related Netty's'ChannelInitializer' and is pretty much unused.
+//        Perhaps a NetconfConnectionFactory? It would:
+//        - take a TransportChannel
+//        - initialize the pipeline with MessageEncoder
+//        - produce a NetconfConnection, which has:
+//          -  TransportChannel transport()
+//          -  MessageEncoder messageEncoder()
 public abstract class AbstractChannelInitializer<S extends NetconfSession> {
     public static final String NETCONF_SESSION_NEGOTIATOR = "negotiator";
 
+    // FIXME: this should be taking only a TransportChannel and should be factory method producing NetconfConnections,
+    //        without the 'attach a negotiator with some promise' part
     public void initialize(final Channel ch, final Promise<S> promise) {
         ch.pipeline().addLast(FrameDecoder.HANDLER_NAME, new EOMFrameDecoder());
         initializeMessageDecoder(ch);
@@ -40,5 +49,7 @@ public abstract class AbstractChannelInitializer<S extends NetconfSession> {
      * Insert session negotiator into the pipeline. It must be inserted after message decoder
      * identified by {@link MessageDecoder#HANDLER_NAME}, (or any other custom decoder processor)
      */
+    // FIXME: This should be somewhere else. It should take an already-initialized NetconfConnection and perform
+    //        the listener magic of eventually giving out the negotiated NetconfSession.
     protected abstract void initializeSessionNegotiator(Channel ch, Promise<S> promise);
 }
index c5fe45f224fc8d02edbf84d8631fd0f6129f8614..ece47d8d309bc531c951e8b76767d936448d1a79 100644 (file)
@@ -28,12 +28,18 @@ import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.re
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+// FIXME: move this class to netconf.common
 public abstract class AbstractNetconfSession<S extends NetconfSession, L extends NetconfSessionListener<S>>
+        // FIXME: This is fugly: we receive either NetconfNotification or Exception, routing it to listener.
+        //        It would be much better if we communicated Exception via something else than channelRead(), for
+        //        example via userEventTriggered(). The contract of what can actually be seen is dictated by
+        //        MessageDecoder in general.
         extends SimpleChannelInboundHandler<Object> implements NetconfSession {
     private static final Logger LOG = LoggerFactory.getLogger(AbstractNetconfSession.class);
 
     private final @NonNull SessionIdType sessionId;
     private final @NonNull L sessionListener;
+    // FIXME: we should have a TransportChannel available
     private final @NonNull Channel channel;
 
     private boolean up;
@@ -45,6 +51,11 @@ public abstract class AbstractNetconfSession<S extends NetconfSession, L extends
         LOG.debug("Session {} created", sessionId);
     }
 
+    @Override
+    public final SessionIdType sessionId() {
+        return sessionId;
+    }
+
     protected abstract S thisInstance();
 
     @Override
@@ -65,6 +76,25 @@ public abstract class AbstractNetconfSession<S extends NetconfSession, L extends
     }
 
     @Override
+    // FIXME: The below comment provides a bit more reasoning why this method should be kept of out end-user APIs.
+    //        This method can be invoked from two different contexts:
+    //        - a user either publishing a NotificationMessage, a RpcMessage, or similar: we know for sure that the
+    //          entrypoint lies outside of the EventLoop's executor
+    //        - the server responding with a RpcReplyMessage or similar: we may or may not be executing on the event
+    //          loop.
+    //
+    //        Examples:
+    //
+    //        DeserializerExceptionHandler.exceptionCaught() is calling SendErrorExceptionUtil.sendErrorMessage().
+    //        Is that okay?
+    //
+    //        NetconfServerSessionListener.onMessage() is catching DocumentedException and calling
+    //        SendErrorExceptionUtil.sendErrorMessage(). Is that okay?
+    //
+    //        In any case we need to ensure requests complete in the order they arrived, I think, otherwise problems may
+    //        occur. Callers of both need to be audited and we should be checking EventLoop.inEventLoop(). Note that we
+    //        could be acquiring a ChannelHandlerContext, so we can find something helpful there?
+    //
     public ChannelFuture sendMessage(final NetconfMessage netconfMessage) {
         // From: https://github.com/netty/netty/issues/3887
         // Netty can provide "ordering" in the following situations:
@@ -116,10 +146,6 @@ public abstract class AbstractNetconfSession<S extends NetconfSession, L extends
         return up;
     }
 
-    public final @NonNull SessionIdType sessionId() {
-        return sessionId;
-    }
-
     @Override
     @SuppressWarnings("checkstyle:illegalCatch")
     public final void channelInactive(final ChannelHandlerContext ctx) {
index dec1821322cc926caee5b409fee7e329519a7ed0..7d039f06f08358b41b4727c7ce7eaa7551bd72c1 100644 (file)
@@ -38,6 +38,15 @@ import org.xml.sax.SAXException;
  * Netconf messages after hello should be processed once the negotiation succeeded.
  *
  */
+// FIXME: This handler conflates two things:
+//        - extracting session metadata
+//        - holding onto non-HelloMessages
+//
+//        The former is a leftover from initial implementation we had SSH provided with http://www.jcraft.com/jsch/ --
+//        where we had piping proxies rather than a live Channel connected to the user, due to JSch being synchronous.
+//        See comments about that in NetconfHelloMessageAdditionalHeader.
+//
+//        The need for the latter should disappear once we split out
 public final class HelloXMLMessageDecoder extends MessageDecoder {
     private static final Logger LOG = LoggerFactory.getLogger(HelloXMLMessageDecoder.class);
 
index 07a68233f807e152f9ece768edda7ae98d93e77b..711b5b673314c7408ab8268c885c0cf726ff74c2 100644 (file)
@@ -9,7 +9,9 @@ package org.opendaylight.netconf.api;
 
 import io.netty.channel.ChannelFuture;
 import java.io.Closeable;
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.opendaylight.netconf.api.messages.NetconfMessage;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.SessionIdType;
 
 /**
  * Protocol Session represents the finite state machine in underlying protocol, including timers and its purpose is to
@@ -20,10 +22,37 @@ import org.opendaylight.netconf.api.messages.NetconfMessage;
  * <p>
  * This interface should be implemented by a final class representing a protocol specific session.
  */
+@NonNullByDefault
 public interface NetconfSession extends Closeable {
+    /**
+     * Return the {@code session-id} of this session.
+     *
+     * @return the {@code session-id} of this session
+     */
+    SessionIdType sessionId();
 
+    // FIXME: This is overly generalized leading to potential bad use:
+    //        - HelloMessage cannot be sent as it is used only before NetconfSession is established
+    //        - RpcMessage can only be sent via ClientSession
+    //        - RpcReply can only be sent on a ServerSession
+    //        - NotificationMessage can only be sent on a ServerSession which has seen a subscription request
+    //
+    //        There are further state management issues: without interleave capability the client gives up the right
+    //        to execute most operations.
+    //
+    //        At the end of the day, this method should be exposed from AbstractNetconfSession only, with perhaps a
+    //        different signature.
     ChannelFuture sendMessage(NetconfMessage message);
 
+    // FIXME: this is ambiguous w.r.t.:
+    //        - protocol interactions:
+    //          - is it <kill-session>, https://www.rfc-editor.org/rfc/rfc6241#section-7.9, available to both parties?
+    //          - is it <close-session>, https://www.rfc-editor.org/rfc/rfc6241#section-7.8, available to client?
+    //        - state transitions:
+    //          - what happens sendMessage()'s still in the output TCP buffer?
+    //          - what happens to those that are on the wire?
+    //        - synchronicity: is this a non-blocking operation?
+    //        - if it is blocking: what is its non-blocking equivalent?
     @Override
     void close();
 }
index 17374ead4d12ded4afc136ff7bdb96774091cf20..d45a3e05ea2e5971fa93917bc58ab8e0ee2b4911 100644 (file)
@@ -28,7 +28,28 @@ import java.util.regex.Pattern;
  * Session-identifier is optional, others mandatory.
  * </pre>
  * This header is inserted in front of a netconf hello message followed by a newline.
+ *
+ * @deprecated This class is a leftover from initial implementation where SSH was done by http://www.jcraft.com/jsch/
+ *             and piped channels to get around the sync nature of JSch. We are now using a directly-connected Netty
+ *             channel via transport-ssh.
  */
+// FIXME: session-identifier is unused
+// FIXME: hostAddress, and port is readily available from Channel
+// FIXME: transport should be visible in the form of TransportChannel in NetconfServerSession, the actual String here
+//        is a leak towards netconf-server-'s ietf-netconf-monitoring operational state. That piece of code should use
+//        a switch expression to go from 'transportChannel instanceof SSHTransportChannel' to 'NetconfSsh.VALUE' without
+//        the intermediate random string -- which does not support TLS anyway!
+// FIXME: userName is coming from:
+//        - authentication in case of SSH/TLS
+//        - from wire in case of TCP
+//        We should propagate these via transport-api:
+//        -  sealed interface TransportUser permits AuthenticatedUser, UnauthenticatedUser
+//        -  non-sealed interface AuthenticatedUser
+//        -  final class UnauthenticatedUser
+//        A TransportUser and TransportChannel via a netconf.codec.NetconfChannel exposed to
+//        ServerSessionNegotiator. On ClientSessionNegotiator side we want to have TransportServer exposing server's
+//        credentials, like the result of server key authentication.
+@Deprecated(since = "8.0.0", forRemoval = true)
 public class NetconfHelloMessageAdditionalHeader {
 
     private static final String SC = ";";
index a00f2a2a36a343ad2e7b4dc5b5e256f14c0dd82f..4dace0a2d2da896714ad2f1b839564bdf80c94df 100644 (file)
@@ -13,6 +13,19 @@ import org.eclipse.jdt.annotation.NonNull;
 /**
  * An channel handler assembling inbound messages from specified framing.
  */
+// TODO: Reconsider use of ByteToMessageDecoder here. While the Cumulator use is nice, I think we can do better if we
+//       do our buffer management ourselves:
+//       - we already use a CompositeByteBuf in ChunkedFrameDecoder with a buch of state tracking
+//       - we have searchIndex in order to optimize resumed searches, so taking ownership of the buffer should not be
+//         a big problem
+//
+//       ChannelInboundHandlerAdapter should be a good first step, allowing us to focus on the implementation rather
+//       than the relationship to MessageDecoder.
+//
+//       Ultimately this should become a base class disconnected from ChannelHandler, instances of which are given out
+//       by FramingSupport to MessageEncoder, which then handles interactions between those objects and the Netty
+//       channel, becoming also ChannelInboundHandler. Note that when that happens, channel initialization (now in
+//       AbstractChannelInitializer) must be updated to place MessageEncoder *before* MessageDecoder.
 public abstract sealed class FrameDecoder extends ByteToMessageDecoder permits ChunkedFrameDecoder, EOMFrameDecoder {
     /**
      * The name of the handler providing frame decoding.
index dc80a7cc1d3118be4e593bfec9d5e64ba93f5f2a..d882808019e3c978379d9999eecbc9bb8c9e3b04 100644 (file)
@@ -14,6 +14,16 @@ import org.opendaylight.netconf.api.messages.NetconfMessage;
 /**
  * A decoder from a series of bytes to a {@link NetconfMessage}.
  */
+// FIXME: 'NetconfMesssage' is not quite accurate: we can also produce Exceptions and the handling of those needs to be
+//        specific to the negotiator or attached client/server session. This class should probably be producting:
+//        -  sealed interface MessageEvent permits MessageException, MessageDocument
+//        -  record DecoderError(Exception cause), or similar
+//        -  record DecoderMessage(Document document), to be converted to NetconfMessage
+//        There should be another abstract class for the above:
+//        -  abstract sealed class MessageHandler extends ChannelInboundHandlerAdapter
+//               permits AbstractNetconfSesssion, AbstractSessionNegotiator
+//        -  with it having a codified channelRead() method which does not tolerate unknown messages and dispatches to
+//           the abstract equivalent of AbstractNetconfSesssion.handleError()/handleMessage()
 public abstract class MessageDecoder extends ByteToMessageDecoder {
     /**
      * The name of the handler providing message decoding.
index 3d5069b3a996b44d9868ebf55d161f7995724903..d76e77af94435f81bb342431e6f068bdb8a9b97a 100644 (file)
@@ -23,6 +23,10 @@ import org.slf4j.LoggerFactory;
 /**
  * An encoder of {@link NetconfMessage} to a series of bytes.
  */
+// TODO: MessageToByteEncoder is forcing us to encode the entire NetconfMessage into a single buffer before it can be
+//       passed downstream. We should switch to a plain ChannelOutboundHandlerAdapter and send multiple ByteBufs down
+//       the pipeline, completing the Promise with the result of Future returned by the last write(ByteBuf) -- which
+//       we expect underlying Channel to handle as required by https://www.rfc-editor.org/rfc/rfc6241#section-2.1
 public final class MessageEncoder extends MessageToByteEncoder<NetconfMessage> {
     private static final Logger LOG = LoggerFactory.getLogger(MessageEncoder.class);
 
index 0a491d6d5dc41a473b5084349321967af323e720..aed26ad2af3e823b2b6ac2b98b36cd398d7a1dbf 100644 (file)
@@ -48,6 +48,8 @@ import org.opendaylight.yangtools.yang.common.Uint32;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+// FIXME: separate out API and implementation, because at it currently stands we are leaking all of
+//        ChannelInboundHandlerAdapter, potentially leading to surprises.
 public final class NetconfServerSession extends AbstractNetconfExiSession<NetconfServerSession,
         NetconfServerSessionListener> implements NetconfManagementSession {
     private static final Logger LOG = LoggerFactory.getLogger(NetconfServerSession.class);
@@ -106,6 +108,21 @@ public final class NetconfServerSession extends AbstractNetconfExiSession<Netcon
         return channelFuture;
     }
 
+    // FIXME: the YANG definition for monitoring says:
+    //
+    //            uses common-counters {
+    //              description
+    //                "Per-session counters.  Zero based with following reset behaviour:
+    //                 - at start of a session
+    //                 - when max value is reached";
+    //            }
+    //
+    //        the overflow should checked after increment: if it becomes Uint32#MAX_VALUE + 1, it needs to
+    //        be reset to 1.
+    //
+    //        We want to isolate the three into a separate class, so that we can share code between here and whoever
+    //        is populating the Statistics container. That class should implement CommonCounters to make it super easy
+    //        to fill via {Session,Statistics}Builder.fieldsFrom(Grouping).
     public void onIncommingRpcSuccess() {
         inRpcSuccess++;
     }
@@ -121,6 +138,8 @@ public final class NetconfServerSession extends AbstractNetconfExiSession<Netcon
     @Override
     public Session toManagementSession() {
         final SessionBuilder builder = new SessionBuilder().withKey(new SessionKey(sessionId().getValue()));
+
+        // FIXME: use channel to get this information
         final InetAddress address1 = InetAddresses.forString(header.getAddress());
         final IpAddress address;
         if (address1 instanceof Inet4Address) {
@@ -139,9 +158,12 @@ public final class NetconfServerSession extends AbstractNetconfExiSession<Netcon
                 .setInBadRpcs(new ZeroBasedCounter32(Uint32.valueOf(inRpcFail)))
                 .setInRpcs(new ZeroBasedCounter32(Uint32.valueOf(inRpcSuccess)))
                 .setOutRpcErrors(new ZeroBasedCounter32(Uint32.valueOf(outRpcError)))
+                // FIXME: a TransportUser from somewhere around TransportChannel
                 .setUsername(header.getUserName())
+                // FIXME: derive from TransportChannel instead
                 .setTransport(getTransportForString(header.getTransport()))
                 .setOutNotifications(new ZeroBasedCounter32(Uint32.valueOf(outNotification)))
+                // FIXME: obsolete this leaf and do not produce it here
                 .addAugmentation(new Session1Builder().setSessionIdentifier(header.getSessionIdentifier()).build())
                 .build();
     }