ensoft / entrance / EnTrance.Feature.Target

EnTrance provides a rich set of functionality for interacting with multiple simultaneous protocol peers (such as high-end routers). Understanding how to use this requires the notion of a "target".

If you don't need to initiate simultaneous protocol sessions to multiple peers, you can skip this module!

Targets

The EnTrance abstraction for something like a router, to which protocol sessions can be opened, is a target. A target is a string identifier, chosen by your app, which disambiguates between multiple protocol endpoints. The name itself doesn't matter - using router1 and router2 will have identical semantics to bill and ted.

If your app talks only to one device at a time, you can just omit anything to do with target names. For example, if you want to execute the CLI command show version on a single router, you can just do this, without ever mentioning target names at all:

import EnTrance.Channel as Channel
import EnTrance.Feature.Target as Target
import EnTrance.Feature.Target.CLI as CLI
import EnTrance.Types exposing (MaybeSubscribe(..))

-- Start the CLI exec feature
CLI.start SubscribeToConState
    |> Channel.send model

-- Connect from the server to the target (the single router)
Target.connect params
    |> Channel.send model

-- Back in your update function, after you get a notification saying
-- the connection state is `Connected`:
CLI.exec "show version"
    |> Channel.sendSimpleRpc model

If you might talk to two or more devices, then you must assign your own target names, and specify the appropriate one for every request. For example:

-- Start the CLI exec feature for the target
CLI.start SubscribeToConState
    |> Target.set "router1"
    |> Channel.send model

-- Connect from the server to the target
Target.connect params
    |> Target.set "router1"
    |> Channel.send model

-- Back in your update function, after you get a notification saying
-- the connection state is `Connected`:
CLI.exec "show version"
    |> Target.set "router1"
    |> Channel.sendSimpleRpc model

The way the first option works, is that EnTrance silently assigns a default target name for you (imaginatively called defaultTarget) unless you override it, as in the second example.

Note that connections are initiated on a per-target basis, not a per-feature basis. So Target.connect initiates outbound connections for all features for the given target (default or specified).

connect : Connection.Params -> EnTrance.Request.Request

Initiate connection requests for all features for a single target. If you're just using the default target, to talk to a single peer device, then just start one or more features, and then call this. eg for a single Netconf session:

Netconf.start
    |> Channel.send model

Target.connect params
    |> Channel.send model

If you're handling multiple targets, then use addTarget. For example:

Netconf.start
    |> Target.set "router1"
    |> Channel.send model

Syslog.start
    |> Target.set "router1"
    |> Channel.send model

Target.connect params
    |> Target.set "router1"
    |> Channel.send model

This is an async request - use the connection state notifications to track progress.

disconnect : EnTrance.Request.Request

Initiate disconnect requests for all features with this target.

This is an async request - use the connection state notifications to track progress.

Connection state

A single protocol session has a unified abstraction of a connection state - eg Connected, Disconnecting, or FailedToConnect. So whether you are talking Netconf over SSH or gNMI over gPRC, you can have a unified user interface to show what's working and what isn't.

Target groups

If you have multiple protocol sessions to the same target (eg you might have both Netconf and a Syslog connections), then creating a target group with the same target name as the Netconf and Syslog feature provides an aggregate entity with two handy properties:

This is so useful in practice that currently connect and disconnect are exposed only for groups. So if you have only a single Netconf session, for example, you still have to create a group with the same target name, in order to connect. File an issue if this actually causes problems. (This is a side-effect of the server-side demux logic.)

For example, if you do this set of requests:

import EnTrance.Feature.Target.CLI as CLI
import EnTrance.Feature.Target.Syslog as Syslog
import EnTrance.Feature.Target.Group as Group

Group.start
    |> Channel.send model

Syslog.start
    |> Channel.send model

Netconf.start
    |> Channel.send model

then these will self-assemble into a hierarchy where the CLI and Netconf features are children of the Group (because they all have the same target name, namely defaultTarget):

 Target-group    [ defaultTarget ]
     ├── Syslog  [ defaultTarget ]
     └── Netconf [ defaultTarget ]

That then means you can call connect or disconnect on just the group, and the individual CLI/Netconf connect/disconnect calls are handled for you, and you can also subscribe to the group connection state.

Target group hierarchies

You can go further, and create arbitrary hierarchies of target groups. So if you had Netconf and Syslog connections to a bunch of routers, themselves grouped into "core" and "edge" groups, then EnTrance might tell you that all the "core" routers are Connected, but the "edge" group is Connecting (if eg one netconf connection to one edge router is Connecting but everything else is Connected).

You invoke this hierarchy functionality simply by providing a "parent group" when creating a new group - this slides the new group under the specified parent. A target group without a parent is the root of its own sub-hierarchy.

For example, if you do this set of requests:

Group.start
    |> Target.set "router1"
    |> Channel.send model

Syslog.start
    |> Target.set "router1"
    |> Channel.send model

Netconf.start
    |> Target.set "router1"
    |> Channel.send model

Group.start
    |> Target.set "router2"
    |> Channel.send model

Syslog.start
    |> Target.set "router2"
    |> Channel.send model

Netconf.start
    |> Target.set "router2"
    |> Channel.send model

then these self-assemble into two isolated hierarchies, based on target name:

 Target-group   [ router1 ]
    ├── Syslog  [ router1 ]
    └── Netconf [ router1 ]

 Target-group   [ router2 ]
    ├── Syslog  [ router2 ]
    └── Netconf [ router2 ]

If you create groups like this instead:

Group.start
    |> Target.set "all-routers"
    |> Channel.send model

Group.startWithParent "all-routers"
    |> Target.set "router1"
    |> Channel.send model

Group.startWithParent "all-routers"
    |> Target.set "router2"
    |> Channel.send model

then you create an additional level of connection state summarisation:

 Target-group            [ all-routers ]
     |
     ├── Target-group    [ router1 ]
     |       ├── Syslog  [ router1 ]
     |       └── Netconf [ router2 ]
     |
     └── Target-group    [ router2 ]
             ├── Syslog  [ router2 ]
             └── Netconf [ router2 ]

Requests and Notifications

By default, all Requests have a default target value, in order to keep a simple API for the vast majority of apps that are not target aware.

If your app is target aware, then you can specify the intended target for each request using add.

set : String -> EnTrance.Request.Request -> EnTrance.Request.Request

Add a target parameter to a request.

decode : Json.Decode.Decoder a -> Json.Decode.Decoder ( String, a )

Extract the target from any notification. This turns the result of any other decoder into a pair, where the first item is the target name.