summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnders Svensson <anders@erlang.org>2019-09-11 15:15:17 +0200
committerAnders Svensson <anders@erlang.org>2019-09-11 15:15:17 +0200
commit965137dece696cadaeee8171c0738dbd560358b7 (patch)
tree7aa023eebe526750b11c8c83191d1239a61372f7
parent415c4818fd4b34c8f2a5fd5728862227ee5e433d (diff)
parent6849e56d5cd40bb44546dce2e5a419d8962ccd20 (diff)
downloaderlang-965137dece696cadaeee8171c0738dbd560358b7.tar.gz
Merge branch 'anders/common_test/NETCONF-1.1/OTP-15789'
* anders/common_test/NETCONF-1.1/OTP-15789: Tweak documentation Fix handling of non-RPC reply Simplify request timers Fix inaccurate comment Replace create_subscription/1-6 in 15 variants Fix documentation/spec of ssh-related options Be flexible with capabilities specification Document connect/hello timeout in one place Let capabilities be set for all session establishment Handle reception of invalid session-id Add ct_netconfc support for NETCONF 1.1 Don't treat reception of NETCONF > 1.0 hello as error
-rw-r--r--lib/common_test/doc/src/ct_netconfc.xml369
-rw-r--r--lib/common_test/src/ct_netconfc.erl1564
2 files changed, 1170 insertions, 763 deletions
diff --git a/lib/common_test/doc/src/ct_netconfc.xml b/lib/common_test/doc/src/ct_netconfc.xml
index 8fbe5f3df6..ab56ea5587 100644
--- a/lib/common_test/doc/src/ct_netconfc.xml
+++ b/lib/common_test/doc/src/ct_netconfc.xml
@@ -4,7 +4,7 @@
<erlref>
<header>
<copyright>
- <year>2010</year><year>2017</year>
+ <year>2010</year><year>2019</year>
<holder>Ericsson AB. All Rights Reserved.</holder>
</copyright>
<legalnotice>
@@ -37,51 +37,64 @@
<description>
- <p>NETCONF client module.</p>
-
- <p>The NETCONF client is compliant with RFC 4741 NETCONF Configuration
- Protocol and RFC 4742 Using the NETCONF Configuration Protocol over
- Secure SHell (SSH).</p>
+ <p>NETCONF client module compliant with RFC 6241, NETCONF Configuration
+ Protocol, and RFC 6242, Using the NETCONF Configuration Protocol over
+ Secure SHell (SSH), and with support for RFC 5277, NETCONF Event
+ Notifications.</p>
<marker id="Connecting"/>
<p><em>Connecting to a NETCONF server</em></p>
- <p>NETCONF sessions can either be opened by a single call
- to <seealso marker="#open-1"><c>open/1,2</c></seealso> or by a call
- to <seealso marker="#connect-1"><c>connect/1,2</c></seealso> followed
- by one or more calls to
- <seealso marker="#session-1"><c>session/1,2,3</c></seealso>.</p>
-
- <p>The properties of the sessions will be exactly the same, except
- that when
- using <seealso marker="#connect-1"><c>connect/1,2</c></seealso>, you
- may start multiple sessions over the same SSH connection. Each
- session is implemented as an SSH channel.</p>
-
- <p><seealso marker="#open-1"><c>open/1,2</c></seealso> will establish one
- SSH connection with one SSH channel implementing one NETCONF
- session. You may start mutiple sessions by
- calling <seealso marker="#open-1"><c>open/1,2</c></seealso> multiple
- times, but then a new SSH connection will be established for each
- session.</p>
-
- <p>For each server to test against, the following entry can be added to a
- configuration file:</p>
+ <p>Call <seealso marker="#connect-1"><c>connect/1,2</c></seealso>
+ to establish a connection to a server, then pass the returned
+ handle to <seealso marker="#session-1"><c>session/1-3</c></seealso> to
+ establish a NETCONF session on a new SSH channel.
+ Each call to
+ <seealso marker="#session-1"><c>session/1-3</c></seealso> establishes a
+ new session on the same connection, and results in a hello message
+ to the server.</p>
+
+ <p>Alternately,
+ <seealso marker="#open-1"><c>open/1,2</c></seealso> can be used to
+ establish a single session on a dedicated connection.
+ (Or, equivalently,
+ <seealso marker="#only_open-1"><c>only_open/1,2</c></seealso>
+ followed by <seealso marker="#hello-1"><c>hello/1-3</c></seealso>.)</p>
+
+ <p>Connect/session options can be specified in a configuration
+ file with entries like the following.</p>
<pre>
- {server_id(),options()}.</pre>
+ {server_id(), [option()]}.</pre>
<p>The <seealso marker="#type-server_id"><c>server_id()</c></seealso>
- or an associated
- <seealso marker="ct#type-target_name"><c>ct:target_name()</c></seealso>
- must then be used in calls to
- <seealso marker="#connect-2"><c>connect/2</c></seealso>
- or <seealso marker="#open-2"><c>open/2</c></seealso>.</p>
-
- <p>If no configuration exists for a server,
- use <seealso marker="#connect-1"><c>connect/1</c></seealso>
- or <seealso marker="#open-1"><c>open/1</c></seealso> instead,
- and specify all necessary options in the <c>Options</c> parameter.</p>
+ or an associated
+ <seealso marker="ct#type-target_name"><c>ct:target_name()</c></seealso>
+ can then be passed to the aforementioned functions to use the
+ referenced configuration.</p>
+
+ <marker id="Signaling"/>
+ <p><em>Signaling</em></p>
+
+ <p>Protocol operations in the NETCONF protocol are realized as remote
+ procedure calls (RPCs) from client to server and a corresponding
+ reply from server to client.
+ RPCs are sent using like-named functions (eg.
+ <seealso marker="#edit_config-3"><c>edit_config/3-5</c></seealso>
+ to send an edit-config RPC), with the server reply
+ as return value.
+ There are functions for each RPC defined in RFC 6241 and
+ the create-subscription RPC from RFC 5277, all of which are
+ wrappers on <seealso marker="#send_rpc-2"><c>send_rpc/2,3</c></seealso>,
+ that can be used to send an arbitrary RPC
+ not defined in RFC 6241 or RFC 5277.</p>
+
+ <p>All of the signaling functions have one variant with a
+ <c>Timeout</c> argument and one without, corresponding to an
+ infinite timeout.
+ The latter is inappropriate in most cases since a non-response by
+ the server or a missing message-id causes the call to hang
+ indefinitely.</p>
<marker id="Logging"/>
<p><em>Logging</em></p>
@@ -93,7 +106,7 @@
<pre>
suite() -&gt;
- [{ct_hooks, [{cth_conn_log, [{<seealso marker="ct#type-conn_log_mod"><c>ct:conn_log_mod()</c></seealso>,<seealso marker="ct#type-conn_log_options"><c>ct:conn_log_options()</c></seealso>}]}]}].</pre>
+ [{ct_hooks, [{cth_conn_log, [{<seealso marker="ct#type-conn_log_mod"><c>ct:conn_log_mod()</c></seealso>, <seealso marker="ct#type-conn_log_options"><c>ct:conn_log_options()</c></seealso>}]}]}].</pre>
<p><c>conn_log_mod()</c> is the name of the <c>Common Test</c> module
implementing the connection protocol, for example, <c>ct_netconfc</c>.</p>
@@ -133,7 +146,7 @@
configuration variable <c>ct_conn_log</c>:</p>
<pre>
- {ct_conn_log,[{<seealso marker="ct#type-conn_log_mod"><c>ct:conn_log_mod()</c></seealso>,<seealso marker="ct#type-conn_log_options"><c>ct:conn_log_options()</c></seealso>}]}.</pre>
+ {ct_conn_log,[{<seealso marker="ct#type-conn_log_mod"><c>ct:conn_log_mod()</c></seealso>, <seealso marker="ct#type-conn_log_options"><c>ct:conn_log_options()</c></seealso>}]}.</pre>
<p>For example:</p>
@@ -185,100 +198,111 @@
would cause HTML logging of all NETCONF connections in to the test
case HTML log.</p>
- <marker id="Notifications"/>
- <p><em>Notifications</em></p>
-
- <p>The NETCONF client is also compliant with RFC 5277 NETCONF Event
- Notifications, which defines a mechanism for an asynchronous message
- notification delivery service for the NETCONF protocol.</p>
-
- <p>Specific functions to support this are
- <seealso marker="#create_subscription-1"><c>create_subscription/1-6</c></seealso>
- and
- <seealso marker="#get_event_streams-1"><c>get_event_streams/1-3</c></seealso>.</p>
-
- <marker id="Default_timeout"/>
- <p><em>Default Timeout</em></p>
-
- <p>Most of the functions in this module have one variant with
- a <c>Timeout</c> parameter, and one without. If nothing else is
- specified, the default value <c>infinity</c> is used when
- the <c>Timeout</c> parameter is not given.</p>
-
</description>
+ <!-- ====================================================================== -->
+
<datatypes>
<datatype>
<name name="client"/>
- </datatype>
- <datatype>
- <name name="error_reason"/>
- </datatype>
- <datatype>
- <name name="event_time"/>
+ <desc>
+ <p>Handle to a NETCONF session, as required by signaling
+ functions.</p>
+ </desc>
</datatype>
<datatype>
<name name="handle"/>
<desc>
- <p>Opaque reference for a connection to a NETCONF server or a
- NETCONF session.</p>
+ <p>Handle to a connection to a NETCONF server as
+ returned by
+ <seealso marker="#connect-1"><c>connect/1,2</c></seealso>,
+ or to a session as returned by
+ <seealso marker="#session-1"><c>session/1-3</c></seealso>,
+ <seealso marker="#open-1"><c>open/1,2</c></seealso>,
+ or <seealso marker="#only_open-1"><c>only_open/1,2</c></seealso>.</p>
</desc>
</datatype>
<datatype>
- <name name="host"/>
- </datatype>
- <datatype>
- <name name="netconf_db"/>
+ <name name="xs_datetime"/>
+ <desc>
+ <p>Date and time of a startTime/stopTime element in an RFC
+ 5277 create-subscription request. Of XML primitive type
+ <c>dateTime</c>, which has the (informal) form</p>
+ <pre>
+ [-]YYYY-MM-DDThh:mm:ss[.s][Z|(+|-)hh:mm]</pre>
+ <p>where <c>T</c> and <c>Z</c> are literal and <c>.s</c> is
+ one or more fractional seconds.</p>
+ </desc>
</datatype>
<datatype>
- <name name="notification"/>
+ <name name="event_time"/>
</datatype>
<datatype>
<name name="notification_content"/>
</datatype>
<datatype>
- <name name="option"/>
+ <name name="notification"/>
<desc>
- <p><c>SshConnectOption</c> is any valid option to
- <seealso marker="ssh:ssh#connect-3"><c>ssh:connect/3,4</c></seealso>.
- Common options used are <c>user</c>, <c>password</c>
- and <c>user_dir</c>. The <c>SshConnectOptions</c> are
- verfied by the SSH application.</p>
+ <p>Event notification messages sent as a result of calls to
+ <seealso marker="#create_subscription-2"><c>create_subscription/2,3</c></seealso>.</p>
</desc>
</datatype>
<datatype>
- <name name="options"/>
+ <name name="option"/>
<desc>
- <p>Options used for setting up an SSH connection to a NETCONF
- server.</p>
+ <p>Options <c>host</c> and <c>port</c> specify the
+ server endpoint to which to connect, and are passed directly
+ to <seealso
+ marker="ssh:ssh#connect-3"><c>ssh:connect/4</c></seealso>,
+ as are arbitrary ssh options. Common options are <c>user</c>,
+ <c>password</c> and <c>user_dir</c>.</p>
+
+ <p>Option <c>timeout</c> specifies the number of
+ milliseconds to allow for connection establishment and, if the
+ function in question results in an outgoing hello message,
+ reception of the server hello. The timeout applies to
+ connection and hello independently;
+ one timeout for connection establishment, another for hello
+ reception.</p>
+
+ <p>Option <c>capability</c> specifies the content of a
+ corresponding element in an outgoing hello message, each
+ option specifying the content of a single element.
+ If no base NETCONF capability is configured then the RFC 4741
+ 1.0 capability, "urn:ietf:params:netconf:base:1.0", is added,
+ otherwise not.
+ In particular, the RFC 6241 1.1 capability must be explicitly
+ configured.
+ NETCONF capabilities can be specified using the shorthand notation
+ defined in RFC 6241, any capability string starting with a
+ colon being prefixed by either "urn:ietf:params:netconf" or
+ "urn:ietf:params:netconf:capability", as appropriate.</p>
+
+ <p>Capability options are ignored by connect/1-3 and only_open/1-2,
+ which don't result in an outgoing hello message.</p>
</desc>
</datatype>
<datatype>
<name name="server_id"/>
<desc>
- <p>The identity of a server, specified in a configuration
- file.</p>
- </desc>
- </datatype>
- <datatype>
- <name name="simple_xml"/>
- <desc>
- <p>This type is further described in application
- <seealso marker="xmerl:index"><c>xmerl</c></seealso>.</p>
+ <p>Identity of connection or session configuration in a
+ configuration file.</p>
</desc>
</datatype>
<datatype>
<name name="stream_data"/>
- <desc>
- <p>For details about the data format for the string values, see
- "XML Schema for Event Notifications" in RFC 5277.</p>
- </desc>
</datatype>
<datatype>
<name name="stream_name"/>
</datatype>
<datatype>
<name name="streams"/>
+ <desc>
+ <p>Stream information as returned by
+ <seealso marker="#get_event_streams-1"><c>get_event_streams/1-3</c></seealso>.
+ See RFC 5277, "XML Schema for Event Notifications", for detail
+ on the format of the string values.</p>
+ </desc>
</datatype>
<datatype>
<name name="xml_attribute_tag"/>
@@ -296,20 +320,28 @@
<name name="xml_tag"/>
</datatype>
<datatype>
+ <name name="simple_xml"/>
+ <desc>
+ <p>Representation of XML, as described in application
+ <seealso marker="xmerl:index"><c>xmerl</c></seealso>.</p>
+ </desc>
+ </datatype>
+ <datatype>
<name name="xpath"/>
</datatype>
<datatype>
- <name name="xs_datetime"/>
- <desc>
- <p>This date and time identifier has the same format as the XML type
- <c>dateTime</c> and is compliant with RFC 3339 Date and Time on
- the Internet Timestamps. The format is as follows:</p>
- <pre>
- [-]CCYY-MM-DDThh:mm:ss[.s][Z|(+|-)hh:mm]</pre>
- </desc>
+ <name name="error_reason"/>
+ </datatype>
+ <datatype>
+ <name name="host"/>
+ </datatype>
+ <datatype>
+ <name name="netconf_db"/>
</datatype>
</datatypes>
+ <!-- ====================================================================== -->
+
<funcs>
<func>
<name name="action" arity="2" since="OTP R15B02"/>
@@ -352,11 +384,7 @@
reference returned from this
function is required as connection identifier when opening
sessions over this connection, see
- <seealso marker="#session-1"><c>session/1,2,3</c></seealso>.</p>
-
- <p>Option <c>timeout</c> (milliseconds) is used when setting up the
- SSH connection. It is not used for any other purposes during the
- lifetime of the connection.</p>
+ <seealso marker="#session-1"><c>session/1-3</c></seealso>.</p>
</desc>
</func>
@@ -371,10 +399,9 @@
<c>target_name()</c> associated with such an Id, then the options
for this server are fetched from the configuration file.</p>
- <p>Argument <c><anno>ExtraOptions</anno></c> is added to the
- options found in the configuration file. If the same options
- are specified, the values from the configuration file
- overwrite <c><anno>ExtraOptions</anno></c>.</p>
+ <p>The options list is added to those of the
+ configuration file. If an option is specified in both lists,
+ the configuration file takes precedence.</p>
<p>If the server is not specified in a configuration file, use
<seealso marker="#connect-1"><c>connect/1</c></seealso>
@@ -384,17 +411,13 @@
reference returned from this
function can be used as connection identifier when opening
sessions over this connection, see
- <seealso marker="#session-1"><c>session/1,2,3</c></seealso>.
+ <seealso marker="#session-1"><c>session/1-3</c></seealso>.
However, if <c><anno>KeyOrName</anno></c> is a
<c>target_name()</c>, that is, if the server is named through a
call to <seealso marker="ct#require-2"><c>ct:require/2</c></seealso>
or a <c>require</c> statement in the test suite, then this name can
be used instead of
<seealso marker="#type-handle"><c>handle()</c></seealso>.</p>
-
- <p>Option <c>timeout</c> (milliseconds) is used when setting up the
- SSH connection. It is not used for any other purposes during the
- lifetime of the connection.</p>
</desc>
</func>
@@ -412,80 +435,54 @@
</func>
<func>
- <name since="OTP R15B02">create_subscription(Client) -> Result</name>
- <name since="OTP R15B02">create_subscription(Client, Stream) -> Result</name>
- <name since="OTP R15B02">create_subscription(Client, Stream, Filter) -> Result</name>
- <name since="OTP R15B02">create_subscription(Client, Stream, Filter, Timeout) -> Result</name>
- <name name="create_subscription" arity="5" clause_i="2" since="OTP R15B02"/>
- <name name="create_subscription" arity="6" since="OTP R15B02"/>
+ <name name="create_subscription" arity="2" clause_i="1"/>
+ <name name="create_subscription" arity="3" clause_i="1"/>
<fsummary>Creates a subscription for event notifications.</fsummary>
<desc>
- <p>Creates a subscription for event notifications.</p>
-
- <p>This function sets up a subscription for NETCONF event
- notifications of the specified stream type, matching the specified
- filter. The calling process receives notifications as messages of
- type <seealso marker="#type-notification"><c>notification()</c></seealso>.</p>
+ <p>Creates a subscription for event notifications by sending
+ an RFC 5277 create-subscription RPC to the server.
+ The calling process receives events as messages of
+ type <seealso marker="#type-notification"><c>notification()</c></seealso>.</p>
- <p>Only a subset of the function clauses are show above. The
- full set of valid combinations of input parameters is as
- follows:</p>
-
-<pre>create_subscription(Client)
-
-create_subscription(Client, Timeout)
-create_subscription(Client, Stream)
-create_subscription(Client, Filter)
-
-create_subscription(Client, Stream, Timeout)
-create_subscription(Client, Filter, Timeout)
-create_subscription(Client, Stream, Filter)
-create_subscription(Client, StartTime, StopTime)
-
-create_subscription(Client, Stream, Filter, Timeout)
-create_subscription(Client, StartTime, StopTime, Timeout)
-create_subscription(Client, Stream, StartTime, StopTime)
-create_subscription(Client, Filter, StartTime, StopTime)
-
-create_subscription(Client, Stream, StartTime, StopTime, Timeout)
-create_subscription(Client, Stream, Filter, StartTime, StopTime)
-create_subscription(Client, Stream, Filter, StartTime, StopTime, Timeout)</pre>
+ <p>From RFC 5722, 2.1 Subscribing to Receive Event Notifications:</p>
<taglist>
<tag><c><anno>Stream</anno></c></tag>
- <item><p>Optional parameter that indicates which stream of event
+ <item><p>Indicates which stream of event
is of interest. If not present, events in the default NETCONF
stream are sent.</p></item>
<tag><c><anno>Filter</anno></c></tag>
- <item><p>Optional parameter that indicates which subset of all
+ <item><p>Indicates which subset of all
possible events is of interest. The parameter format is the
same as that of the filter parameter in the NETCONF protocol
operations. If not present, all events not precluded by other
parameters are sent.</p></item>
<tag><c><anno>StartTime</anno></c></tag>
- <item><p>Optional parameter used to trigger the replay feature and
+ <item><p>Used to trigger the replay feature and
indicate that the replay is to start at the time specified.
If <c><anno>StartTime</anno></c> is not present, this is not a
- replay subscription.</p>
- <p>It is not valid to specify start times that are later than
+ replay subscription.
+ It is not valid to specify start times that are later than
the current time. If <c><anno>StartTime</anno></c> is specified
earlier than the log can support, the replay begins with the
- earliest available notification.</p>
- <p>This parameter is of type <c>dateTime</c> and compliant to
+ earliest available notification.
+ This parameter is of type <c>dateTime</c> and compliant to
RFC 3339. Implementations must support time zones.</p></item>
<tag><c><anno>StopTime</anno></c></tag>
- <item><p>Optional parameter used with the optional replay feature
+ <item><p>Used with the optional replay feature
to indicate the newest notifications of interest. If
<c><anno>StopTime</anno></c> is not present, the notifications
- continues until the subscription is terminated.</p>
- <p>Must be used with and be later than <c>StartTime</c>. Values
+ continues until the subscription is terminated.
+ Must be used with and be later than <c>StartTime</c>. Values
of <c><anno>StopTime</anno></c> in the future are valid. This
parameter is of type <c>dateTime</c> and compliant to RFC 3339.
Implementations must support time zones.</p></item>
</taglist>
- <p>For more details about the event notification mechanism, see
- RFC 5277.</p>
+ <p>See RFC 5277 for more details. The requirement that
+ <c>StopTime</c> must only be used with <c>StartTime</c> is not
+ enforced, to allow an invalid request to be sent to the
+ server.</p>
</desc>
</func>
@@ -561,23 +558,7 @@ create_subscription(Client, Stream, Filter, StartTime, StopTime, Timeout)</pre>
<name name="get_capabilities" arity="2" since="OTP R15B02"/>
<fsummary>Returns the server side capabilities.</fsummary>
<desc>
- <p>Returns the server side capabilities.</p>
-
- <p>The following capability identifiers, defined in RFC 4741 NETCONF
- Configuration Protocol, can be returned:</p>
-
- <list>
- <item><p><c>"urn:ietf:params:netconf:base:1.0"</c></p></item>
- <item><p><c>"urn:ietf:params:netconf:capability:writable-running:1.0"</c></p></item>
- <item><p><c>"urn:ietf:params:netconf:capability:candidate:1.0"</c></p></item>
- <item><p><c>"urn:ietf:params:netconf:capability:confirmed-commit:1.0"</c></p></item>
- <item><p><c>"urn:ietf:params:netconf:capability:rollback-on-error:1.0"</c></p></item>
- <item><p><c>"urn:ietf:params:netconf:capability:startup:1.0"</c></p></item>
- <item><p><c>"urn:ietf:params:netconf:capability:url:1.0"</c></p></item>
- <item><p><c>"urn:ietf:params:netconf:capability:xpath:1.0"</c></p></item>
- </list>
-
- <p>More identifiers can exist, for example, server-side namespace.</p>
+ <p>Returns the server capabilities as received in its hello message.</p>
</desc>
</func>
@@ -652,10 +633,12 @@ create_subscription(Client, Stream, Filter, StartTime, StopTime, Timeout)</pre>
<name name="hello" arity="3" since="OTP 17.5.3"/>
<fsummary>Exchanges hello messages with the server.</fsummary>
<desc>
- <p>Exchanges <c>hello</c> messages with the server.</p>
+ <p>Exchanges <c>hello</c> messages with the server. Returns
+ when the server hello has been received or after the
+ specified timeout.</p>
- <p>Adds optional capabilities and sends a <c>hello</c> message to the
- server and waits for the return.</p>
+ <p>Note that capabilities for an outgoing hello can be passed
+ directly to <seealso marker="#open-2"><c>open/2</c></seealso>.</p>
</desc>
</func>
@@ -740,11 +723,6 @@ create_subscription(Client, Stream, Filter, StartTime, StopTime, Timeout)</pre>
reference returned from this
function is required as client identifier when calling any other
function in this module.</p>
-
- <p>Option <c>timeout</c> (milliseconds) is used when setting up the
- SSH connection and when waiting for the <c>hello</c> message from
- the server. It is not used for any other purposes during the
- lifetime of the connection.</p>
</desc>
</func>
@@ -761,10 +739,9 @@ create_subscription(Client, Stream, Filter, StartTime, StopTime, Timeout)</pre>
<c>target_name()</c> associated with such an Id, then the options
for this server are fetched from the configuration file.</p>
- <p>Argument <c><anno>ExtraOptions</anno></c> is added to the
- options found in the configuration file. If the same
- options are specified, the values from the configuration
- file overwrite <c><anno>ExtraOptions</anno></c>.</p>
+ <p>The options list is added to those of the
+ configuration file. If an option is specified in both lists,
+ the configuration file take precedence.</p>
<p>If the server is not specified in a configuration file, use
<seealso marker="#open-1"><c>open/1</c></seealso>
@@ -780,11 +757,6 @@ create_subscription(Client, Stream, Filter, StartTime, StopTime, Timeout)</pre>
be used instead of
<seealso marker="#type-handle"><c>handle()</c></seealso>.</p>
- <p>Option <c>timeout</c> (milliseconds) is used when setting up the
- SSH connection and when waiting for the <c>hello</c> message from
- the server. It is not used for any other purposes during the
- lifetime of the connection.</p>
-
<p>See also
<seealso marker="ct#require-2"><c>ct:require/2</c></seealso>.</p>
</desc>
@@ -827,7 +799,6 @@ create_subscription(Client, Stream, Filter, StartTime, StopTime, Timeout)</pre>
<fsummary>Opens a NETCONF session as a channel on the given SSH
connection, and exchanges hello messages with the
server.</fsummary>
- <type name="session_options"/>
<type name="session_option"/>
<desc>
<p>Opens a NETCONF session as a channel on the given SSH
diff --git a/lib/common_test/src/ct_netconfc.erl b/lib/common_test/src/ct_netconfc.erl
index 6a758c4ea3..7ad6fa46e8 100644
--- a/lib/common_test/src/ct_netconfc.erl
+++ b/lib/common_test/src/ct_netconfc.erl
@@ -1,7 +1,7 @@
%%----------------------------------------------------------------------
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2012-2017. All Rights Reserved.
+%% Copyright Ericsson AB 2012-2019. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -26,7 +26,7 @@
%% Netconf servers can be configured by adding the following statement
%% to a configuration file:
%%
-%% {server_id(),options()}.
+%% {server_id(), [option()]}.
%%
%% The server_id() or an associated ct:target_name() shall then be
%% used in calls to open/2 connect/2.
@@ -55,13 +55,14 @@
%% The netconf client is also compliant with RFC5277 NETCONF Event
%% Notifications, which defines a mechanism for an asynchronous
%% message notification delivery service for the netconf protocol.
-%%
-%% Specific functions to support this are create_subscription/6
-%% get_event_streams/3. (The functions also exist with other arities.)
+%% Functions supporting this are create_subscription/3
+%% get_event_streams/3.
%%
%%----------------------------------------------------------------------
-module(ct_netconfc).
+-dialyzer(no_improper_lists).
+
-include("ct_netconfc.hrl").
-include("ct_util.hrl").
-include_lib("xmerl/include/xmerl.hrl").
@@ -107,12 +108,8 @@
copy_config/4,
action/2,
action/3,
- create_subscription/1,
create_subscription/2,
create_subscription/3,
- create_subscription/4,
- create_subscription/5,
- create_subscription/6,
get_event_streams/1,
get_event_streams/2,
get_event_streams/3,
@@ -121,6 +118,12 @@
get_session_id/1,
get_session_id/2]).
+%% historic, no longer documented
+-export([create_subscription/1,
+ create_subscription/4,
+ create_subscription/5,
+ create_subscription/6]).
+
%%----------------------------------------------------------------------
%% Exported types
%%----------------------------------------------------------------------
@@ -163,6 +166,9 @@
(is_atom(Xml) orelse (is_tuple(Xml) andalso is_atom(element(1,Xml))))).
-define(is_string(S), (is_list(S) andalso is_integer(hd(S)))).
+%% Keys into the process dictionary.
+-define(KEY(T), {?MODULE, T}).
+
%%----------------------------------------------------------------------
%% Records
%%----------------------------------------------------------------------
@@ -173,9 +179,9 @@
capabilities,
session_id,
msg_id = 1,
- hello_status,
- no_end_tag_buff = <<>>,
- buff = <<>>,
+ hello_status, % undefined | received | #pending{}
+ % string() | {error, Reason}
+ buf = false, % binary() | list() | boolean()
pending = [], % [#pending]
event_receiver}).% pid
@@ -195,10 +201,9 @@
type}).
%% Pending replies from server
--record(pending, {tref, % timer ref (returned from timer:xxx)
- ref, % pending ref
+-record(pending, {tref :: false | reference(), % timer reference
msg_id,
- op,
+ op,
caller}).% pid which sent the request
%%----------------------------------------------------------------------
@@ -207,13 +212,14 @@
-type client() :: handle() | server_id() | ct:target_name().
-opaque handle() :: pid().
--type options() :: [option()].
--type option() :: {ssh,host()} | {port,inet:port_number()} | {user,string()} |
- {password,string()} | {user_dir,string()} |
- {timeout,timeout()}.
+-type option() :: {host | ssh, host()}
+ | {port, inet:port_number()}
+ | {timeout, timeout()}
+ | {capability, string() | [string()]}
+ | ssh:client_option().
--type session_options() :: [session_option()].
--type session_option() :: {timeout,timeout()}.
+-type session_option() :: {timeout,timeout()}
+ | {capability, string() | [string()]}.
-type host() :: inet:hostname() | inet:ip_address().
@@ -258,33 +264,44 @@
%% Open an SSH connection to a Netconf server
%% If the server options are specified in a configuration file, use
%% open/2.
+
+%% connect/1
+
-spec connect(Options) -> Result when
- Options :: options(),
- Result :: {ok,handle()} | {error,error_reason()}.
+ Options :: [option()],
+ Result :: {ok, handle()} | {error, error_reason()}.
connect(Options) ->
- do_connect(Options, #options{type=connection},[]).
+ connect(Options, #options{type = connection}, []).
+
+%% connect/2
--spec connect(KeyOrName,ExtraOptions) -> Result when
+-spec connect(KeyOrName, ExtraOptions) -> Result when
KeyOrName :: ct:key_or_name(),
- ExtraOptions :: options(),
- Result :: {ok,handle()} | {error,error_reason()}.
+ ExtraOptions :: [option()],
+ Result :: {ok, handle()} | {error, error_reason()}.
+
connect(KeyOrName, ExtraOptions) ->
- SortedExtra = lists:keysort(1,ExtraOptions),
- SortedConfig = lists:keysort(1,ct:get_config(KeyOrName,[])),
- AllOpts = lists:ukeymerge(1,SortedConfig,SortedExtra),
- do_connect(AllOpts,#options{name=KeyOrName,type=connection},[{name,KeyOrName}]).
-
-do_connect(OptList,InitOptRec,NameOpt) ->
- case check_options(OptList,InitOptRec) of
- {Host,Port,Options} ->
- ct_gen_conn:start({Host,Port},Options,?MODULE,
- NameOpt ++ [{reconnect,false},
- {use_existing_connection,false},
- {forward_messages,false}]);
- Error ->
- Error
+ connect(make_opts(KeyOrName, ExtraOptions),
+ #options{name = KeyOrName, type = connection},
+ [{name, KeyOrName}]).
+
+%% connect/3
+
+connect(Opts, InitRec, NameOpt) ->
+ case make_options(Opts, InitRec) of
+ #options{} = Rec ->
+ start(Rec, NameOpt, false);
+ {error, _} = No ->
+ No
end.
+%% make_opts/2
+
+make_opts(KeyOrName, ExtraOptions) ->
+ SortedExtra = lists:keysort(1, ExtraOptions),
+ SortedConfig = lists:keysort(1, ct:get_config(KeyOrName, [])),
+ lists:ukeymerge(1, SortedConfig, SortedExtra).
+
%%----------------------------------------------------------------------
%% Close the given SSH connection.
-spec disconnect(Conn) -> ok | {error,error_reason()} when
@@ -300,146 +317,185 @@ disconnect(Conn) ->
%%----------------------------------------------------------------------
%% Open a netconf session as a channel on the given SSH connection,
%% and exchange `hello' messages.
+
+%% session/1
+
-spec session(Conn) -> Result when
Conn :: handle(),
- Result :: {ok,handle()} | {error,error_reason()}.
+ Result :: {ok, handle()} | {error, error_reason()}.
+
session(Conn) ->
- do_session(Conn,[],#options{type=channel},[]).
+ session(Conn, [], #options{type = channel}, []).
--spec session(Conn,Options) -> Result when
+%% session/2
+
+-spec session(Conn, Options) -> Result when
Conn :: handle(),
- Options :: session_options(),
- Result :: {ok,handle()} | {error,error_reason()};
- (KeyOrName,Conn) -> Result when
+ Options :: [session_option()],
+ Result :: {ok, handle()} | {error, error_reason()};
+ (KeyOrName, Conn) -> Result when
KeyOrName :: ct:key_or_name(),
Conn :: handle(),
- Result :: {ok,handle()} | {error,error_reason()}.
-session(Conn,Options) when is_list(Options) ->
- do_session(Conn,Options,#options{type=channel},[]);
-session(KeyOrName,Conn) ->
- do_session(Conn,[],#options{name=KeyOrName,type=channel},[{name,KeyOrName}]).
+ Result :: {ok, handle()} | {error, error_reason()}.
+
+session(Conn, Options) when is_list(Options) ->
+ session(Conn, Options, #options{type = channel}, []);
--spec session(KeyOrName,Conn,Options) -> Result when
+session(KeyOrName, Conn) ->
+ session(Conn,
+ [],
+ #options{name = KeyOrName, type = channel},
+ [{name, KeyOrName}]).
+
+%% session/3
+
+-spec session(KeyOrName, Conn, Options) -> Result when
Conn :: handle(),
- Options :: session_options(),
+ Options :: [session_option()],
KeyOrName :: ct:key_or_name(),
- Result :: {ok,handle()} | {error,error_reason()}.
-session(KeyOrName,Conn,ExtraOptions) ->
- SortedExtra = lists:keysort(1,ExtraOptions),
- SortedConfig = lists:keysort(1,ct:get_config(KeyOrName,[])),
- AllOpts = lists:ukeymerge(1,SortedConfig,SortedExtra),
- do_session(Conn,AllOpts,#options{name=KeyOrName,type=channel},
- [{name,KeyOrName}]).
-
-do_session(Conn,OptList,InitOptRec,NameOpt) ->
- case call(Conn,get_ssh_connection) of
- {ok,SshConn} ->
- case check_session_options(OptList,InitOptRec) of
- {ok,Options} ->
- case ct_gen_conn:start(SshConn,Options,?MODULE,
- NameOpt ++
- [{reconnect,false},
- {use_existing_connection,false},
- {forward_messages,true}]) of
- {ok,Client} ->
- case hello(Client,Options#options.timeout) of
- ok ->
- {ok,Client};
- Error ->
- Error
- end;
- Error ->
- Error
- end;
- Error ->
- Error
- end;
- Error ->
- Error
+ Result :: {ok, handle()} | {error, error_reason()}.
+
+session(KeyOrName, Conn, ExtraOptions) ->
+ session(Conn,
+ make_opts(KeyOrName, ExtraOptions),
+ #options{name = KeyOrName, type = channel},
+ [{name, KeyOrName}]).
+
+%% session/4
+
+session(Conn, Opts, InitRec, NameOpt) ->
+ T = make_ref(),
+ try
+ [_ | {ok, SshConn}] = [T | call(Conn, get_ssh_connection)],
+ [_ | #options{} = Rec] = [T | make_session_options(Opts, InitRec)],
+ [_ | {ok, Client} = Ok] = [T | start(SshConn, Rec, NameOpt, true)],
+ [_ | ok] = [T | hello(Client, caps(Opts), Rec#options.timeout)],
+ Ok
+ catch
+ error: {badmatch, [T | Error]} ->
+ Error
end.
+%% caps/1
+
+caps(Opts) ->
+ [T || {capability, _} = T <- Opts].
+
%%----------------------------------------------------------------------
%% Open a netconf session and exchange 'hello' messages.
%% If the server options are specified in a configuration file, use
%% open/2.
+
+%% open/1
+
-spec open(Options) -> Result when
- Options :: options(),
- Result :: {ok,handle()} | {error,error_reason()}.
+ Options :: [option()],
+ Result :: {ok, handle()} | {error, error_reason()}.
+
open(Options) ->
- open(Options,#options{type=connection_and_channel},[],true).
+ open(Options,
+ #options{type = connection_and_channel},
+ [],
+ true).
--spec open(KeyOrName, ExtraOptions) -> Result when
+-spec open(KeyOrName, ExtraOption) -> Result when
KeyOrName :: ct:key_or_name(),
- ExtraOptions :: options(),
- Result :: {ok,handle()} | {error,error_reason()}.
+ ExtraOption :: [option()],
+ Result :: {ok, handle()} | {error, error_reason()}.
+
open(KeyOrName, ExtraOpts) ->
open(KeyOrName, ExtraOpts, true).
-open(KeyOrName, ExtraOpts, Hello) ->
- SortedExtra = lists:keysort(1,ExtraOpts),
- SortedConfig = lists:keysort(1,ct:get_config(KeyOrName,[])),
- AllOpts = lists:ukeymerge(1,SortedConfig,SortedExtra),
- open(AllOpts,#options{name=KeyOrName,type=connection_and_channel},
- [{name,KeyOrName}],Hello).
-
-open(OptList,InitOptRec,NameOpt,Hello) ->
- case check_options(OptList,InitOptRec) of
- {Host,Port,Options} ->
- case ct_gen_conn:start({Host,Port},Options,?MODULE,
- NameOpt ++ [{reconnect,false},
- {use_existing_connection,false},
- {forward_messages,true}]) of
- {ok,Client} when Hello==true ->
- case hello(Client,Options#options.timeout) of
- ok ->
- {ok,Client};
- Error ->
- Error
- end;
- Other ->
- Other
- end;
- Error ->
- Error
+%% open/3
+
+open(KeyOrName, ExtraOptions, Hello) ->
+ open(make_opts(KeyOrName, ExtraOptions),
+ #options{name = KeyOrName, type = connection_and_channel},
+ [{name, KeyOrName}],
+ Hello).
+
+%% open/4
+
+open(Opts, InitRec, NameOpt, Hello) ->
+ T = make_ref(),
+ try
+ [_, #options{} = Rec] = [T, make_options(Opts, InitRec)],
+ [_, {ok, Client} = Ok | true] = [T, start(Rec, NameOpt, true) | Hello],
+ [_, ok] = [T, hello(Client, caps(Opts), Rec#options.timeout)],
+ Ok
+ catch
+ error: {badmatch, [T, Res | _]} ->
+ Res
end.
+%% start/3
+
+start(#options{host = undefined}, _, _) ->
+ {error, no_host_address};
+
+start(#options{port = undefined}, _, _) ->
+ {error, no_port};
+
+start(#options{host = Host, port = Port} = Opts, NameOpt, Fwd) ->
+ start({Host, Port}, Opts, NameOpt, Fwd).
+
+%% start/4
+
+start(Ep, Opts, NameOpt, Fwd) ->
+ ct_gen_conn:start(Ep, Opts, ?MODULE, [{reconnect, false},
+ {use_existing_connection, false},
+ {forward_messages, Fwd}
+ | NameOpt]).
%%----------------------------------------------------------------------
-%% As open/1,2, except no 'hello' message is sent.
+%% Like open/1,2, but no 'hello' message is sent.
+
-spec only_open(Options) -> Result when
- Options :: options(),
- Result :: {ok,handle()} | {error,error_reason()}.
+ Options :: [option()],
+ Result :: {ok, handle()} | {error, error_reason()}.
+
only_open(Options) ->
- open(Options,#options{type=connection_and_channel},[],false).
+ open(Options, #options{type = connection_and_channel}, [], false).
--spec only_open(KeyOrName,ExtraOptions) -> Result when
+-spec only_open(KeyOrName, ExtraOptions) -> Result when
KeyOrName :: ct:key_or_name(),
- ExtraOptions :: options(),
- Result :: {ok,handle()} | {error,error_reason()}.
+ ExtraOptions :: [option()],
+ Result :: {ok, handle()} | {error, error_reason()}.
+
only_open(KeyOrName, ExtraOpts) ->
open(KeyOrName, ExtraOpts, false).
%%----------------------------------------------------------------------
%% Send a 'hello' message.
+
+%% hello/1
+
-spec hello(Client) -> Result when
Client :: handle(),
- Result :: ok | {error,error_reason()}.
+ Result :: ok | {error, error_reason()}.
+
hello(Client) ->
- hello(Client,[],?DEFAULT_TIMEOUT).
+ hello(Client, [], ?DEFAULT_TIMEOUT).
+
+%% hello/2
--spec hello(Client,Timeout) -> Result when
+-spec hello(Client, Timeout) -> Result when
Client :: handle(),
Timeout :: timeout(),
- Result :: ok | {error,error_reason()}.
-hello(Client,Timeout) ->
- hello(Client,[],Timeout).
+ Result :: ok | {error, error_reason()}.
+
+hello(Client, Timeout) ->
+ hello(Client, [], Timeout).
--spec hello(Client,Options,Timeout) -> Result when
+%% hello/3
+
+-spec hello(Client, Options, Timeout) -> Result when
Client :: handle(),
Options :: [{capability, [string()]}],
Timeout :: timeout(),
- Result :: ok | {error,error_reason()}.
-hello(Client,Options,Timeout) ->
+ Result :: ok | {error, error_reason()}.
+
+hello(Client, Options, Timeout) ->
call(Client, {hello, Options, Timeout}).
@@ -675,117 +731,122 @@ action(Client,Action,Timeout) ->
%%----------------------------------------------------------------------
%% Send a 'create-subscription' request
%% See RFC5277, NETCONF Event Notifications
--spec create_subscription(Client) -> Result when
- Client :: client(),
- Result :: ok | {error,error_reason()}.
-create_subscription(Client) ->
- create_subscription(Client,?DEFAULT_STREAM,?DEFAULT_TIMEOUT).
--spec create_subscription(Client, Stream | Filter | Timeout) -> Result when
+%% create_subscription/2
+
+-spec create_subscription(Client, Values) -> Result when
Client :: client(),
+ Values :: #{stream => Stream,
+ filter => Filter,
+ start => StartTime,
+ stop => StopTime},
Stream :: stream_name(),
Filter :: simple_xml() | [simple_xml()],
- Timeout :: timeout(),
+ StartTime :: xs_datetime(),
+ StopTime :: xs_datetime(),
+ Result :: ok | {error,error_reason()};
+ %% historic, no longer documented
+ (Client, list() | timeout()) -> Result when
+ Client :: client(),
Result :: ok | {error,error_reason()}.
-create_subscription(Client,Timeout)
+
+create_subscription(Client, #{} = Values) ->
+ create_subscription(Client, Values, ?DEFAULT_TIMEOUT);
+
+%% historic clauses
+create_subscription(Client, Timeout)
when ?is_timeout(Timeout) ->
- create_subscription(Client,?DEFAULT_STREAM,Timeout);
-create_subscription(Client,Stream)
+ create_subscription(Client, #{}, Timeout);
+create_subscription(Client, Stream)
when ?is_string(Stream) ->
- create_subscription(Client,Stream,?DEFAULT_TIMEOUT);
-create_subscription(Client,Filter)
+ create_subscription(Client, #{stream => Stream});
+create_subscription(Client, Filter)
when ?is_filter(Filter) ->
- create_subscription(Client,?DEFAULT_STREAM,Filter,
- ?DEFAULT_TIMEOUT).
+ create_subscription(Client, #{filter => Filter}).
-create_subscription(Client,Stream,Timeout)
- when ?is_string(Stream) andalso
- ?is_timeout(Timeout) ->
- call(Client,{send_rpc_op,{create_subscription,self()},
- [Stream,undefined,undefined,undefined],
- Timeout});
-create_subscription(Client,StartTime,StopTime)
- when ?is_string(StartTime) andalso
- ?is_string(StopTime) ->
- create_subscription(Client,?DEFAULT_STREAM,StartTime,StopTime,
- ?DEFAULT_TIMEOUT);
-create_subscription(Client,Filter,Timeout)
- when ?is_filter(Filter) andalso
- ?is_timeout(Timeout) ->
- create_subscription(Client,?DEFAULT_STREAM,Filter,Timeout);
-create_subscription(Client,Stream,Filter)
- when ?is_string(Stream) andalso
- ?is_filter(Filter) ->
- create_subscription(Client,Stream,Filter,?DEFAULT_TIMEOUT).
-
-create_subscription(Client,StartTime,StopTime,Timeout)
- when ?is_string(StartTime) andalso
- ?is_string(StopTime) andalso
- ?is_timeout(Timeout) ->
- create_subscription(Client,?DEFAULT_STREAM,StartTime,StopTime,Timeout);
-create_subscription(Client,Stream,StartTime,StopTime)
- when ?is_string(Stream) andalso
- ?is_string(StartTime) andalso
- ?is_string(StopTime) ->
- create_subscription(Client,Stream,StartTime,StopTime,?DEFAULT_TIMEOUT);
-create_subscription(Client,Filter,StartTime,StopTime)
- when ?is_filter(Filter) andalso
- ?is_string(StartTime) andalso
- ?is_string(StopTime) ->
- create_subscription(Client,?DEFAULT_STREAM,Filter,
- StartTime,StopTime,?DEFAULT_TIMEOUT);
-create_subscription(Client,Stream,Filter,Timeout)
- when ?is_string(Stream) andalso
- ?is_filter(Filter) andalso
- ?is_timeout(Timeout) ->
- call(Client,{send_rpc_op,{create_subscription,self()},
- [Stream,Filter,undefined,undefined],
- Timeout}).
-
--spec create_subscription(Client, Stream, StartTime, StopTime, Timeout) ->
- Result when
+-spec create_subscription(Client, Values, Timeout) -> Result when
Client :: client(),
+ Values :: #{stream => Stream,
+ filter => Filter,
+ start => StartTime,
+ stop => StopTime},
Stream :: stream_name(),
+ Filter :: simple_xml() | [simple_xml()],
StartTime :: xs_datetime(),
StopTime :: xs_datetime(),
Timeout :: timeout(),
Result :: ok | {error,error_reason()};
- (Client, Stream, Filter,StartTime, StopTime) ->
- Result when
+ %% historic, no longer documented
+ (Client, list(), list() | timeout()) -> Result when
Client :: client(),
- Stream :: stream_name(),
- Filter :: simple_xml() | [simple_xml()],
- StartTime :: xs_datetime(),
- StopTime :: xs_datetime(),
Result :: ok | {error,error_reason()}.
-create_subscription(Client,Stream,StartTime,StopTime,Timeout)
- when ?is_string(Stream) andalso
- ?is_string(StartTime) andalso
- ?is_string(StopTime) andalso
+
+create_subscription(Client, #{} = Values, Timeout) ->
+ Keys = [{stream, ?DEFAULT_STREAM},
+ {filter, undefined},
+ {start, undefined},
+ {stop, undefined}],
+ call(Client, {send_rpc_op, {create_subscription, self()},
+ [maps:get(K, Values, D) || {K,D} <- Keys],
+ Timeout});
+
+%% historic clauses, arity 3
+create_subscription(Client, Stream, Timeout)
+ when ?is_string(Stream), ?is_timeout(Timeout) ->
+ create_subscription(Client, #{stream => Stream}, Timeout);
+create_subscription(Client, StartTime, StopTime)
+ when ?is_string(StartTime), ?is_string(StopTime) ->
+ create_subscription(Client, #{start => StartTime, stop => StopTime});
+create_subscription(Client, Filter, Timeout)
+ when ?is_filter(Filter), ?is_timeout(Timeout) ->
+ create_subscription(Client, #{filter => Filter}, Timeout);
+create_subscription(Client, Stream, Filter)
+ when ?is_string(Stream), ?is_filter(Filter) ->
+ create_subscription(Client, #{stream => Stream, filter => Filter}).
+
+%% historic clauses, arity 1,4-5
+create_subscription(Client) ->
+ create_subscription(Client, #{}).
+create_subscription(Client, StartTime, StopTime, Timeout)
+ when ?is_string(StartTime), ?is_string(StopTime), ?is_timeout(Timeout) ->
+ Values = #{start => StartTime,
+ stop => StopTime},
+ create_subscription(Client, Values, Timeout);
+create_subscription(Client, Stream, StartTime, StopTime)
+ when ?is_string(Stream), ?is_string(StartTime), ?is_string(StopTime) ->
+ create_subscription(Client, #{stream => Stream,
+ start => StartTime,
+ stop => StopTime});
+create_subscription(Client, Filter, StartTime, StopTime)
+ when ?is_filter(Filter), ?is_string(StartTime), ?is_string(StopTime) ->
+ create_subscription(Client, #{filter => Filter,
+ start => StartTime,
+ stop => StopTime});
+create_subscription(Client, Stream, Filter, Timeout)
+ when ?is_string(Stream), ?is_filter(Filter), ?is_timeout(Timeout) ->
+ Values = #{stream => Stream,
+ filter => Filter},
+ create_subscription(Client, Values, Timeout).
+create_subscription(Client, Stream, StartTime, StopTime, Timeout)
+ when ?is_string(Stream), ?is_string(StartTime), ?is_string(StopTime),
?is_timeout(Timeout) ->
- call(Client,{send_rpc_op,{create_subscription,self()},
- [Stream,undefined,StartTime,StopTime],
- Timeout});
-create_subscription(Client,Stream,Filter,StartTime,StopTime)
- when ?is_string(Stream) andalso
- ?is_filter(Filter) andalso
- ?is_string(StartTime) andalso
+ Values = #{stream => Stream,
+ start => StartTime,
+ stop => StopTime},
+ create_subscription(Client, Values, Timeout);
+create_subscription(Client, Stream, Filter, StartTime, StopTime)
+ when ?is_string(Stream), ?is_filter(Filter), ?is_string(StartTime),
?is_string(StopTime) ->
- create_subscription(Client,Stream,Filter,StartTime,StopTime,?DEFAULT_TIMEOUT).
-
--spec create_subscription(Client, Stream, Filter,StartTime, StopTime, Timeout) ->
- Result when
- Client :: client(),
- Stream :: stream_name(),
- Filter :: simple_xml() | [simple_xml()],
- StartTime :: xs_datetime(),
- StopTime :: xs_datetime(),
- Timeout :: timeout(),
- Result :: ok | {error,error_reason()}.
-create_subscription(Client,Stream,Filter,StartTime,StopTime,Timeout) ->
- call(Client,{send_rpc_op,{create_subscription, self()},
- [Stream,Filter,StartTime,StopTime],
- Timeout}).
+ create_subscription(Client, #{stream => Stream,
+ filter => Filter,
+ start => StartTime,
+ stop => StopTime}).
+create_subscription(Client, Stream, Filter, StartTime, StopTime, Timeout) ->
+ Values = #{stream => Stream,
+ filter => Filter,
+ start => StartTime,
+ stop => StopTime},
+ create_subscription(Client, Values, Timeout).
%%----------------------------------------------------------------------
%% Send a request to get the given event streams
@@ -859,6 +920,8 @@ kill_session(Client, SessionId, Timeout) ->
%% Callback functions
%%----------------------------------------------------------------------
+%% init/3
+
init(_KeyOrName,{CM,{Host,Port}},Options) ->
case ssh_channel(#connection{reference=CM,host=Host,port=Port},Options) of
{ok,Connection} ->
@@ -883,29 +946,32 @@ init(_KeyOrName,{_Host,_Port},Options) ->
{error,Reason}
end.
+%% terminate/2
terminate(_, #state{connection=Connection}) ->
ssh_close(Connection),
ok.
-handle_msg({hello, Options, Timeout}, From,
- #state{connection=Connection,hello_status=HelloStatus} = State) ->
+%% handle_msg/3
+
+%% Send hello and return to the caller only after reception of the
+%% server's hello.
+handle_msg({hello, Options, Timeout},
+ From,
+ #state{connection = Connection,
+ hello_status = HelloStatus}
+ = State) ->
case do_send(Connection, client_hello(Options)) of
- ok ->
- case HelloStatus of
- undefined ->
- {Ref,TRef} = set_request_timer(Timeout),
- {noreply, State#state{hello_status=#pending{tref=TRef,
- ref=Ref,
- caller=From}}};
- received ->
- {reply, ok, State#state{hello_status=done}};
- {error,Reason} ->
- {stop, {error,Reason}, State}
- end;
- Error ->
+ ok when HelloStatus == undefined -> %% server hello not yet received
+ TRef = set_request_timer(Timeout, hello),
+ {noreply, State#state{hello_status = #pending{tref = TRef,
+ caller = From}}};
+ ok -> %% or yes: negotiate version
+ handle_capx(State);
+ Error ->
{stop, Error, State}
end;
+
handle_msg(get_ssh_connection, _From, #state{connection=Connection}=State) ->
Reply =
case Connection#connection.reference of
@@ -914,29 +980,40 @@ handle_msg(get_ssh_connection, _From, #state{connection=Connection}=State) ->
Connection#connection.port}}}
end,
{reply, Reply, State};
-handle_msg(_, _From, #state{session_id=undefined} = State) ->
- %% Hello is not yet excanged - this shall never happen
- {reply,{error,waiting_for_hello},State};
+
+%% Request before server hello. Possible with only_open, since a
+%% handle is then returned without waiting for the server.
+handle_msg(_, _From, #state{session_id = undefined} = State) ->
+ {reply, {error, waiting_for_hello}, State};
+
handle_msg(get_capabilities, _From, #state{capabilities = Caps} = State) ->
{reply, Caps, State};
+
handle_msg(get_session_id, _From, #state{session_id = Id} = State) ->
{reply, Id, State};
-handle_msg({send, Timeout, SimpleXml}, From,
- #state{connection=Connection,pending=Pending} = State) ->
+
+handle_msg({send, Timeout, SimpleXml},
+ From,
+ #state{connection = Connection,
+ pending = Pending}
+ = State) ->
case do_send(Connection, SimpleXml) of
- ok ->
- {Ref,TRef} = set_request_timer(Timeout),
- {noreply, State#state{pending=[#pending{tref=TRef,
- ref=Ref,
- caller=From} | Pending]}};
- Error ->
- {reply, Error, State}
+ ok ->
+ TRef = set_request_timer(Timeout, send),
+ {noreply, State#state{pending = [#pending{tref = TRef,
+ caller = From}
+ | Pending]}};
+ Error ->
+ {reply, Error, State}
end;
+
handle_msg({send_rpc, SimpleXml, Timeout}, From, State) ->
do_send_rpc(undefined, SimpleXml, Timeout, From, State);
+
handle_msg({send_rpc_op, Op, Data, Timeout}, From, State) ->
SimpleXml = encode_rpc_operation(Op,Data),
do_send_rpc(Op, SimpleXml, Timeout, From, State);
+
handle_msg({get_event_streams=Op,Streams,Timeout}, From, State) ->
Filter = {netconf,?NETMOD_NOTIF_NAMESPACE_ATTR,
[{streams,[{stream,[{name,[Name]}]} || Name <- Streams]}]},
@@ -945,7 +1022,9 @@ handle_msg({get_event_streams=Op,Streams,Timeout}, From, State) ->
handle_msg({ssh_cm, CM, {data, Ch, _Type, Data}}, State) ->
ssh_connection:adjust_window(CM,Ch,size(Data)),
+ log(State#state.connection, recv, Data),
handle_data(Data, State);
+
handle_msg({ssh_cm, _CM, _SshCloseMsg}, State) ->
%% _SshCloseMsg can probably be one of
%% {eof,Ch}
@@ -962,21 +1041,29 @@ handle_msg({ssh_cm, _CM, _SshCloseMsg}, State) ->
%%! connection - due to terminate/2
{stop, State};
-handle_msg({Ref,timeout},
- #state{hello_status=#pending{ref=Ref,caller=Caller}} = State) ->
- ct_gen_conn:return(Caller,{error,{hello_session_failed,timeout}}),
- {stop,State#state{hello_status={error,timeout}}};
-handle_msg({Ref,timeout},#state{pending=Pending} = State) ->
- {value,#pending{op=Op,caller=Caller},Pending1} =
- lists:keytake(Ref,#pending.ref,Pending),
- ct_gen_conn:return(Caller,{error,timeout}),
- R = case Op of
- close_session -> stop;
- _ -> noreply
- end,
- %% Halfhearted try to get in correct state, this matches
- %% the implementation before this patch
- {R,State#state{pending=Pending1, no_end_tag_buff= <<>>, buff= <<>>}}.
+
+handle_msg({timeout, TRef, hello},
+ #state{hello_status = #pending{tref = TRef,
+ caller = From}}
+ = State) ->
+ ct_gen_conn:return(From, {error, {hello_session_failed, timeout}}),
+ {stop, State#state{hello_status = {error,timeout}}};
+
+handle_msg({timeout, TRef, Op}, #state{pending = Pending} = State) ->
+ case lists:keytake(TRef, #pending.tref, Pending) of
+ {value, #pending{caller = From}, Rest} ->
+ ct_gen_conn:return(From, {error, timeout}),
+ %% Discard received bytes in hope that the server has sent
+ %% an incomplete message. Otherwise this is doomed to
+ %% leave the connection in an unusable state.
+ {if Op == close_session -> stop; true -> noreply end,
+ State#state{pending = Rest,
+ buf = is_binary(State#state.buf)}};
+ false ->
+ {noreply, State}
+ end.
+
+%% close/1
%% Called by ct_util_server to close registered connections before terminate.
close(Client) ->
@@ -1048,63 +1135,163 @@ get_handle(Client) ->
Error
end.
-check_options(OptList,Options) ->
- check_options(OptList,undefined,undefined,Options).
+%% make_options/2
-check_options([], undefined, _Port, _Options) ->
- {error, no_host_address};
-check_options([], _Host, undefined, _Options) ->
- {error, no_port};
-check_options([], Host, Port, Options) ->
- {Host,Port,Options};
-check_options([{ssh, Host}|T], _, Port, Options) ->
- check_options(T, Host, Port, Options#options{host=Host});
-check_options([{port,Port}|T], Host, _, Options) ->
- check_options(T, Host, Port, Options#options{port=Port});
-check_options([{timeout, Timeout}|T], Host, Port, Options)
- when is_integer(Timeout); Timeout==infinity ->
- check_options(T, Host, Port, Options#options{timeout = Timeout});
-check_options([{timeout, _} = Opt|_T], _Host, _Port, _Options) ->
- {error, {invalid_option, Opt}};
-check_options([Opt|T], Host, Port, #options{ssh=SshOpts}=Options) ->
- %% Option verified by ssh
- check_options(T, Host, Port, Options#options{ssh=[Opt|SshOpts]}).
-
-check_session_options([],Options) ->
- {ok,Options};
-check_session_options([{timeout, Timeout}|T], Options)
- when is_integer(Timeout); Timeout==infinity ->
- check_session_options(T, Options#options{timeout = Timeout});
-check_session_options([Opt|_T], _Options) ->
- {error, {invalid_option, Opt}}.
+make_options(Opts, Rec) ->
+ make_options(Opts, Rec#options{port = undefined}, fun opt/2).
+
+opt({T, Host}, Rec)
+ when T == ssh;
+ T == host ->
+ Rec#options{host = Host};
+
+opt({port, Port}, Rec) ->
+ Rec#options{port = Port};
+
+opt({timeout, Tmo}, Rec)
+ when is_integer(Tmo);
+ Tmo == infinity ->
+ Rec#options{timeout = Tmo};
+
+opt({timeout, _} = T, _) ->
+ throw(T);
+
+opt({capability, _}, Rec) ->
+ Rec;
+
+opt(Opt, #options{ssh = Opts} = Rec) -> %% option verified by ssh
+ Rec#options{ssh = [Opt | Opts]}.
+
+%% make_session_options/2
+
+make_session_options(Opts, Rec) ->
+ make_options(Opts, Rec, fun session_opt/2).
+
+session_opt({capability, _}, Rec) ->
+ Rec;
+
+session_opt({timeout, Tmo}, Rec)
+ when is_integer(Tmo);
+ Tmo == infinity ->
+ Rec#options{timeout = Tmo};
+session_opt(T, _Rec) ->
+ throw(T).
+
+%% make_options/3
+
+make_options(Opts, Rec, F) ->
+ try
+ #options{} = lists:foldl(F, Rec, Opts)
+ catch
+ T ->
+ {error, {invalid_option, T}}
+ end.
%%%-----------------------------------------------------------------
-set_request_timer(infinity) ->
- {undefined,undefined};
-set_request_timer(T) ->
- Ref = make_ref(),
- {ok,TRef} = timer:send_after(T,{Ref,timeout}),
- {Ref,TRef}.
+
+set_request_timer(infinity, _) ->
+ false;
+
+set_request_timer(Tmo, Op) ->
+ erlang:start_timer(Tmo, self(), Op).
%%%-----------------------------------------------------------------
-cancel_request_timer(undefined,undefined) ->
+
+cancel_request_timer(false) ->
ok;
-cancel_request_timer(Ref,TRef) ->
- _ = timer:cancel(TRef),
- receive {Ref,timeout} -> ok
- after 0 -> ok
- end.
+
+cancel_request_timer(TRef) ->
+ erlang:cancel_timer(TRef).
%%%-----------------------------------------------------------------
-client_hello(Options) when is_list(Options) ->
- UserCaps = [{capability, UserCap} ||
- {capability, UserCap} <- Options,
- is_list(hd(UserCap))],
- {hello, ?NETCONF_NAMESPACE_ATTR,
- [{capabilities,
- [{capability,[?NETCONF_BASE_CAP++?NETCONF_BASE_CAP_VSN]}|
- UserCaps]}]}.
+
+%% client_hello/1
+%%
+%% Prepend the 1.0 base capability only if none is specified by the
+%% user. Store the versions in the process dictionary until they're
+%% examined upon reception of server capabilities in handle_capx/1.
+
+client_hello(Opts)
+ when is_list(Opts) ->
+ UserCaps = [{T, cap(lists:flatten(Cs))} || {capability = T, Cs} <- Opts],
+ Vsns = versions(UserCaps),
+ put(?KEY(protocol_vsn), Vsns),
+ {hello,
+ ?NETCONF_NAMESPACE_ATTR,
+ [{capabilities, [{capability, [?NETCONF_BASE_CAP, ?NETCONF_BASE_CAP_VSN]}
+ || [] == Vsns]
+ ++ UserCaps}]}.
+
+%% cap/1
+%%
+%% Let NETCONF capabilities be specified in the shorthand documented in
+%% RFC 6241.
+
+%% This shorthand is documented in RFC 6241 10.4 NETCONF Capabilities
+%% URNS, but not in 8 Capabilities.
+cap(":base:" ++ _ = Str) ->
+ ["urn:ietf:params:netconf", Str];
+
+cap([$:|_] = Str) ->
+ ["urn:ietf:params:netconf:capability", Str];
+
+cap(Str) ->
+ [Str].
+
+%% versions/1
+%%
+%% Extract base protocol versions from capability options.
+
+versions(Opts) ->
+ [V || {capability, L} <- Opts,
+ S <- L,
+ ?NETCONF_BASE_CAP ++ X <- [lists:flatten(S)],
+ V <- [lists:takewhile(fun(C) -> C /= $? end, X)]].
+
+%% handle_capx/1
+%%
+%% Ignore parameters as RFC 6241 (NETCONF 1.1) requires in 8.1
+%% Capabilities Exchange. Be overly lenient with whitespace since RFC
+%% 6241 gives examples with significant trailing whitespace.
+
+handle_capx(#state{hello_status = received, capabilities = Caps} = S) ->
+ Remote = [V || ?NETCONF_BASE_CAP ++ X <- Caps,
+ [V|_] <- [string:lexemes(X, "? \t\r\n")]],
+ Local = erase(?KEY(protocol_vsn)),
+ case protocol_vsn(Local, Remote) of
+ false when Remote == [] ->
+ Reason = {incorrect_hello, no_base_capability_found},
+ {stop, {error, Reason}, S};
+ false ->
+ Reason = {incompatible_base_capability_vsn, lists:min(Remote)},
+ {stop, {error, Reason}, S};
+ Vsn ->
+ put(?KEY(chunk), Vsn /= "1.0"),
+ {reply, ok, rebuf(Vsn, S#state{hello_status = Vsn})}
+ end;
+
+handle_capx(#state{hello_status = {error, _} = No} = S) ->
+ {stop, No, S}.
+
+%% rebuf/2
+%%
+%% Turn the message buffer into a list for 1.1 chunking if the
+%% negotiated protocol version is > 1.0.
+
+rebuf("1.0", S) ->
+ S;
+
+rebuf(_, #state{buf = Bin} = S) ->
+ S#state{buf = [Bin, 3]}.
+
+%% protocol_vsn/2
+
+protocol_vsn([], Vsns) ->
+ protocol_vsn(["1.0"], Vsns);
+
+protocol_vsn(Local, Remote) ->
+ lists:max([false | [V || V <- Remote, lists:member(V, Local)]]).
%%%-----------------------------------------------------------------
@@ -1150,111 +1337,130 @@ maybe_element(Tag,Value) ->
%%%-----------------------------------------------------------------
%%% Send XML data to server
-do_send_rpc(PendingOp,SimpleXml,Timeout,Caller,
- #state{connection=Connection,msg_id=MsgId,pending=Pending} = State) ->
- case do_send_rpc(Connection, MsgId, SimpleXml) of
- ok ->
- {Ref,TRef} = set_request_timer(Timeout),
- {noreply, State#state{msg_id=MsgId+1,
- pending=[#pending{tref=TRef,
- ref=Ref,
- msg_id=MsgId,
- op=PendingOp,
- caller=Caller} | Pending]}};
- Error ->
- {reply, Error, State#state{msg_id=MsgId+1}}
+do_send_rpc(Op, SimpleXml, Timeout, Caller, #state{connection = Connection,
+ msg_id = MsgId,
+ pending = Pending}
+ = State) ->
+ Msg = {rpc,
+ [{'message-id', MsgId} | ?NETCONF_NAMESPACE_ATTR],
+ [SimpleXml]},
+ Next = MsgId + 1,
+ case do_send(Connection, Msg) of
+ ok ->
+ TRef = set_request_timer(Timeout, Op),
+ Rec = #pending{tref = TRef,
+ msg_id = MsgId,
+ op = Op,
+ caller = Caller},
+ {noreply, State#state{msg_id = Next,
+ pending = [Rec | Pending]}};
+ Error ->
+ {reply, Error, State#state{msg_id = Next}}
end.
-do_send_rpc(Connection, MsgId, SimpleXml) ->
- do_send(Connection,
- {rpc,
- [{'message-id',MsgId} | ?NETCONF_NAMESPACE_ATTR],
- [SimpleXml]}).
+do_send(Connection, Simple) ->
+ ssh_send(Connection, frame(to_xml(Simple))).
-do_send(Connection, SimpleXml) ->
- Xml=to_xml_doc(SimpleXml),
- ssh_send(Connection, Xml).
-
-to_xml_doc(Simple) ->
+to_xml(Simple) ->
Prolog = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
- Xml = unicode:characters_to_binary(
- xmerl:export_simple([Simple],
- xmerl_xml,
- [#xmlAttribute{name=prolog,
- value=Prolog}])),
- <<Xml/binary,?END_TAG/binary>>.
+ Chars = xmerl:export_simple([Simple],
+ xmerl_xml,
+ [#xmlAttribute{name = prolog,
+ value = Prolog}]),
+ unicode:characters_to_binary(Chars).
+
+%% frame/1
+
+frame(Bin) ->
+ case get(?KEY(chunk)) of
+ true -> %% 1.1 chunking
+ [chunk(Bin) | "\n##\n"];
+ _ -> %% 1.0 framing
+ [Bin | ?END_TAG]
+ end.
+
+%% chunk/1
+%%
+%% Chunk randomly to exercise the server.
+
+chunk(<<>>) ->
+ [];
+
+chunk(Bin) ->
+ Sz = min(rand:uniform(1024), size(Bin)),
+ <<B:Sz/binary, Rest/binary>> = Bin,
+ ["\n#", integer_to_list(Sz), $\n, B | chunk(Rest)].
%%%-----------------------------------------------------------------
%%% Parse and handle received XML data
-%%% Two buffers are used:
-%%% * 'no_end_tag_buff' contains data that is checked and does not
-%%% contain any (part of an) end tag.
-%%% * 'buff' contains all other saved data - it may or may not
-%%% include (a part of) an end tag.
-%%% The reason for this is to avoid running binary:split/3 multiple
-%%% times on the same data when it does not contain an end tag. This
-%%% can be a considerable optimation in the case when a lot of data is
-%%% received (e.g. when fetching all data from a node) and the data is
-%%% sent in multiple ssh packages.
-handle_data(NewData,#state{connection=Connection} = State0) ->
- log(Connection,recv,NewData),
- NoEndTag0 = State0#state.no_end_tag_buff,
- Buff0 = State0#state.buff,
- Data = <<Buff0/binary, NewData/binary>>,
- case binary:split(Data,?END_TAG,[]) of
- [_NoEndTagFound] ->
- NoEndTagSize = case byte_size(Data) of
- Sz when Sz<5 -> 0;
- Sz -> Sz-5
- end,
- <<NoEndTag1:NoEndTagSize/binary,Buff/binary>> = Data,
- NoEndTag = <<NoEndTag0/binary,NoEndTag1/binary>>,
- {noreply, State0#state{no_end_tag_buff=NoEndTag, buff=Buff}};
- [FirstMsg0,Buff1] ->
- FirstMsg = remove_initial_nl(<<NoEndTag0/binary,FirstMsg0/binary>>),
- SaxArgs = [{event_fun,fun sax_event/3}, {event_state,[]}],
- case xmerl_sax_parser:stream(FirstMsg, SaxArgs) of
- {ok, Simple, _Thrash} ->
- case decode(Simple, State0#state{no_end_tag_buff= <<>>,
- buff=Buff1}) of
- {noreply, #state{buff=Buff} = State} when Buff =/= <<>> ->
- %% Recurse if we have more data in buffer
- handle_data(<<>>, State);
- Other ->
- Other
- end;
- {fatal_error,_Loc,Reason,_EndTags,_EventState} ->
- ?error(Connection#connection.name,
- [{parse_error,Reason},
- {buffer, Buff0},
- {new_data,NewData}]),
- handle_error(Reason, State0#state{no_end_tag_buff= <<>>,
- buff= <<>>})
- end
+
+handle_data(Bin, #state{buf = Head} = S) ->
+ case recv(Bin, Head) of
+ {error, Reason} ->
+ Conn = S#state.connection,
+ ?error(Conn#connection.name, [{receive_error, Reason},
+ {buffer, Head},
+ {bytes, Bin}]),
+ {stop, S};
+ {Bytes, Rest} ->
+ handle_more(Rest, handle_xml(Bytes, S));
+ Buf ->
+ {noreply, S#state{buf = Buf}}
end.
+%% handle_more/2
+
+handle_more(_, {stop, _} = No) ->
+ No;
+
+handle_more(Bin, {noreply, State}) ->
+ handle_data(Bin, State#state{buf = true == get(?KEY(chunk))}).
+
+%% handle_xml/2
+
+handle_xml(Bytes, State) ->
+ case parse(Bytes) of
+ {ok, Simple, _Rest} -> %% ignore trailing bytes
+ decode(Simple, State);
+ {fatal_error,_Loc,Reason,_EndTags,_EventState} ->
+ Conn = State#state.connection,
+ ?error(Conn#connection.name, [{parse_error, Reason},
+ {message, Bytes}]),
+ {noreply, handle_error(Reason, State)}
+ end.
+
+%% parse/1
+
+parse(Bytes) ->
+ xmerl_sax_parser:stream(<<>>, [{event_fun, fun sax_event/3},
+ {event_state, []},
+ {continuation_fun, fun cont/1},
+ {continuation_state, Bytes}]).
+
+%% cont/1
-%% xml does not accept a leading nl and some netconf server add a nl after
-%% each ?END_TAG, ignore them
-remove_initial_nl(<<"\n", Data/binary>>) ->
- remove_initial_nl(Data);
-remove_initial_nl(Data) ->
- Data.
-
-handle_error(Reason, State) ->
- Pending1 = case State#state.pending of
- [] -> [];
- Pending ->
- %% Assuming the first request gets the
- %% first answer
- P=#pending{tref=TRef,ref=Ref,caller=Caller} =
- lists:last(Pending),
- cancel_request_timer(Ref,TRef),
- Reason1 = {failed_to_parse_received_data,Reason},
- ct_gen_conn:return(Caller,{error,Reason1}),
- lists:delete(P,Pending)
- end,
- {noreply, State#state{pending=Pending1}}.
+cont([] = No) ->
+ {<<>>, No};
+
+cont([Bin | Rest]) ->
+ {Bin, Rest};
+
+cont(Bin) ->
+ {Bin, <<>>}.
+
+%% handle_error/2
+
+handle_error(_Reason, #state{pending = []} = State) ->
+ State;
+
+handle_error(Reason, #state{pending = Pending} = State) ->
+ %% Assuming the first request gets the first answer.
+ Rec = #pending{tref = TRef,
+ caller = Caller}
+ = lists:last(Pending),
+ cancel_request_timer(TRef),
+ ct_gen_conn:return(Caller,{error, {failed_to_parse_received_data, Reason}}),
+ State#state{pending = lists:delete(Rec, Pending)}.
%% Event function for the sax parser. It builds a simple XML structure.
%% Care is taken to keep namespace attributes and prefixes as in the original XML.
@@ -1305,136 +1511,180 @@ parse_attrs([]) ->
%%%-----------------------------------------------------------------
-%%% Decoding of parsed XML data
-decode({Tag,Attrs,_}=E, #state{connection=Connection,pending=Pending}=State) ->
- ConnName = Connection#connection.name,
- case get_local_name_atom(Tag) of
- 'rpc-reply' ->
- case get_msg_id(Attrs) of
- undefined ->
- case Pending of
- [#pending{msg_id=MsgId}] ->
- ?error(ConnName,[{warning,rpc_reply_missing_msg_id},
- {assuming,MsgId}]),
- decode_rpc_reply(MsgId,E,State);
- _ ->
- ?error(ConnName,[{error,rpc_reply_missing_msg_id}]),
- {noreply,State}
- end;
- MsgId ->
- decode_rpc_reply(MsgId,E,State)
- end;
- hello ->
- case State#state.hello_status of
- undefined ->
- case decode_hello(E) of
- {ok,SessionId,Capabilities} ->
- {noreply,State#state{session_id = SessionId,
- capabilities = Capabilities,
- hello_status = received}};
- {error,Reason} ->
- {noreply,State#state{hello_status = {error,Reason}}}
- end;
- #pending{tref=TRef,ref=Ref,caller=Caller} ->
- cancel_request_timer(Ref,TRef),
- case decode_hello(E) of
- {ok,SessionId,Capabilities} ->
- ct_gen_conn:return(Caller,ok),
- {noreply,State#state{session_id = SessionId,
- capabilities = Capabilities,
- hello_status = done}};
- {error,Reason} ->
- ct_gen_conn:return(Caller,{error,Reason}),
- {stop,State#state{hello_status={error,Reason}}}
- end;
- Other ->
- ?error(ConnName,[{got_unexpected_hello,E},
- {hello_status,Other}]),
- {noreply,State}
- end;
- notification ->
- EventReceiver = State#state.event_receiver,
- EventReceiver ! E,
- {noreply,State};
- Other ->
- %% Result of send/2, when not sending an rpc request - or
- %% if netconf server sends noise. Can handle this only if
- %% there is just one pending that matches (i.e. has
- %% undefined msg_id and op)
- case [P || P = #pending{msg_id=undefined,op=undefined} <- Pending] of
- [#pending{tref=TRef,ref=Ref,caller=Caller}] ->
- cancel_request_timer(Ref,TRef),
- ct_gen_conn:return(Caller,E),
- {noreply,State#state{pending=[]}};
- _ ->
- ?error(ConnName,[{got_unexpected_msg,Other},
- {expecting,Pending}]),
- {noreply,State}
- end
+%% decode/2
+%%
+%% Decode parsed (incoming) XML.
+
+decode({Tag, _, _} = E, #state{} = State) ->
+ case decode(get_local_name_atom(Tag), E, State) of
+ #state{} = S ->
+ {noreply, S};
+ {stop, #state{}} = T ->
+ T
+ end.
+
+%% decode/3
+
+decode('rpc-reply', {_, Attrs, _} = E, State) ->
+ decode_rpc_reply(get_msg_id(Attrs), E, State);
+
+%% Incoming hello, outgoing not yet sent.
+decode(hello, E, #state{hello_status = undefined} = State) ->
+ case decode_hello(E) of
+ {ok, SessionId, Capabilities} ->
+ State#state{session_id = SessionId,
+ capabilities = Capabilities,
+ hello_status = received};
+ {error, _Reason} = No ->
+ State#state{hello_status = No}
+ end;
+
+%% Incoming hello, outgoing already sent: negotiate protocol version.
+decode(hello, E, #state{hello_status = #pending{tref = TRef,
+ caller = From}}
+ = State) ->
+ cancel_request_timer(TRef),
+ case decode_hello(E) of
+ {ok, SessionId, Capabilities} ->
+ reply(From, handle_capx(State#state{session_id = SessionId,
+ capabilities = Capabilities,
+ hello_status = received}));
+ {error, _Reason} = No ->
+ ct_gen_conn:return(From, No),
+ {stop, State#state{hello_status = No}}
+ end;
+
+%% Duplicate hello: ignore.
+decode(hello, E, #state{hello_status = Other} = State) ->
+ ConnName = (State#state.connection)#connection.name,
+ ?error(ConnName, [{got_unexpected_hello, E},
+ {hello_status, Other}]),
+ State;
+
+decode(notification, E, State) ->
+ State#state.event_receiver ! E,
+ State;
+
+decode(Other, E, State) ->
+ decode_send({got_unexpected_msg, Other}, E, State).
+
+%% reply/2
+%%
+%% Explicitly send a reply that can't be returned.
+
+reply(From, {T, Res, State}) ->
+ ct_gen_conn:return(From, Res),
+ case T of
+ reply ->
+ State;
+ stop ->
+ {T, State}
end.
+%% get_msg_id/1
+
get_msg_id(Attrs) ->
- case lists:keyfind('message-id',1,Attrs) of
- {_,Str} ->
- list_to_integer(Str);
- false ->
- undefined
+ case find('message-id', Attrs) of
+ {_,Str} ->
+ list_to_integer(Str);
+ false ->
+ undefined
end.
-decode_rpc_reply(MsgId,{_,Attrs,Content0}=E,#state{pending=Pending} = State) ->
- case lists:keytake(MsgId,#pending.msg_id,Pending) of
- {value, #pending{tref=TRef,ref=Ref,op=Op,caller=Caller}, Pending1} ->
- cancel_request_timer(Ref,TRef),
- Content = forward_xmlns_attr(Attrs,Content0),
- {CallerReply,{ServerReply,State2}} =
- do_decode_rpc_reply(Op,Content,State#state{pending=Pending1}),
- ct_gen_conn:return(Caller,CallerReply),
- {ServerReply,State2};
- false ->
- %% Result of send/2, when receiving a correct
- %% rpc-reply. Can handle this only if there is just one
- %% pending that matches (i.e. has undefined msg_id and op)
- case [P || P = #pending{msg_id=undefined,op=undefined} <- Pending] of
- [#pending{tref=TRef,
- ref=Ref,
- msg_id=undefined,
- op=undefined,
- caller=Caller}] ->
- cancel_request_timer(Ref,TRef),
- ct_gen_conn:return(Caller,E),
- {noreply,State#state{pending=[]}};
- _ ->
- ConnName = (State#state.connection)#connection.name,
- ?error(ConnName,[{got_unexpected_msg_id,MsgId},
- {expecting,Pending}]),
- {noreply,State}
- end
+%% recode_rpc_reply/3
+
+decode_rpc_reply(undefined, E, #state{pending = [#pending{msg_id = MsgId}]}
+ = State)
+ when MsgId /= undefined ->
+ ConnName = (State#state.connection)#connection.name,
+ ?error(ConnName, [{warning, rpc_reply_missing_msg_id},
+ {assuming, MsgId}]),
+ decode_rpc_reply(MsgId, E, State);
+
+decode_rpc_reply(undefined, _, State) ->
+ ConnName = (State#state.connection)#connection.name,
+ ?error(ConnName, [{error, rpc_reply_missing_msg_id}]),
+ State;
+
+decode_rpc_reply(MsgId,
+ {_, Attrs, Content0}
+ = E,
+ #state{pending = Pending}
+ = State) ->
+ case lists:keytake(MsgId, #pending.msg_id, Pending) of
+ {value, Rec, Rest} ->
+ #pending{tref = TRef, op = Op, caller = From}
+ = Rec,
+ cancel_request_timer(TRef),
+ Content = forward_xmlns_attr(Attrs, Content0),
+ {Reply, T} = do_decode_rpc_reply(Op,
+ Content,
+ State#state{pending = Rest}),
+ ct_gen_conn:return(From, Reply),
+ T;
+ false -> %% not a send_rcp or server has sent wrong id
+ decode_send({got_unexpected_msg_id, MsgId}, E, State)
+ end.
+
+%% decode_send/2
+%%
+%% Result of send/2,3. Only handle one at a time there since all
+%% pendings have msg_id = undefined.
+
+decode_send(ErrorT, Elem, #state{pending = Pending} = State) ->
+ case [P || #pending{msg_id = undefined} = P <- Pending] of
+ [Rec] ->
+ #pending{tref = TRef,
+ caller = From}
+ = Rec,
+ cancel_request_timer(TRef),
+ ct_gen_conn:return(From, Elem),
+ State#state{pending = lists:delete(Rec, Pending)};
+ _ ->
+ Conn = State#state.connection,
+ ?error(Conn#connection.name, [ErrorT, {expecting, Pending}]),
+ State
end.
-do_decode_rpc_reply(Op,Result,State)
- when Op==lock; Op==unlock; Op==edit_config; Op==delete_config;
- Op==copy_config; Op==kill_session ->
- {decode_ok(Result),{noreply,State}};
-do_decode_rpc_reply(Op,Result,State)
- when Op==get; Op==get_config; Op==action ->
- {decode_data(Result),{noreply,State}};
-do_decode_rpc_reply(close_session,Result,State) ->
+%% do_decode_rpc_reply/3
+
+do_decode_rpc_reply(Op, Result, State)
+ when Op == lock;
+ Op == unlock;
+ Op == edit_config;
+ Op == delete_config;
+ Op == copy_config;
+ Op == kill_session ->
+ {decode_ok(Result), State};
+
+do_decode_rpc_reply(Op, Result, State)
+ when Op == get;
+ Op == get_config;
+ Op == action ->
+ {decode_data(Result), State};
+
+do_decode_rpc_reply(close_session, Result, State) ->
case decode_ok(Result) of
- ok -> {ok,{stop,State}};
- Other -> {Other,{noreply,State}}
+ ok ->
+ {ok, {stop, State}};
+ Other ->
+ {Other, State}
end;
-do_decode_rpc_reply({create_subscription,Caller},Result,State) ->
+
+do_decode_rpc_reply({create_subscription, From}, Result, State) ->
case decode_ok(Result) of
- ok ->
- {ok,{noreply,State#state{event_receiver=Caller}}};
- Other ->
- {Other,{noreply,State}}
+ ok ->
+ {ok, State#state{event_receiver = From}};
+ Other ->
+ {Other, State}
end;
-do_decode_rpc_reply(get_event_streams,Result,State) ->
- {decode_streams(decode_data(Result)),{noreply,State}};
-do_decode_rpc_reply(undefined,Result,State) ->
- {Result,{noreply,State}}.
+
+do_decode_rpc_reply(get_event_streams, Result, State) ->
+ {decode_streams(decode_data(Result)), State};
+
+do_decode_rpc_reply(undefined, Result, State) ->
+ {Result, State}.
@@ -1454,7 +1704,7 @@ decode_data([{Tag,Attrs,Content}]) ->
case get_local_name_atom(Tag) of
ok ->
%% when action has return type void
- ok;
+ ok;
data ->
%% Since content of data has nothing from the netconf
%% namespace, we remove the parent's xmlns attribute here
@@ -1525,41 +1775,43 @@ get_all_xmlns_attrs([{Key,_}=Attr|Attrs],XmlnsAttrs) ->
get_all_xmlns_attrs([],XmlnsAttrs) ->
XmlnsAttrs.
-
%% Decode server hello to pick out session id and capabilities
-decode_hello({hello,_Attrs,Hello}) ->
- case lists:keyfind('session-id',1,Hello) of
- {'session-id',_,[SessionId]} ->
- case lists:keyfind(capabilities,1,Hello) of
- {capabilities,_,Capabilities} ->
- case decode_caps(Capabilities,[],false) of
- {ok,Caps} ->
- {ok,list_to_integer(SessionId),Caps};
- Error ->
- Error
- end;
- false ->
- {error,{incorrect_hello,capabilities_not_found}}
- end;
- false ->
- {error,{incorrect_hello,no_session_id_found}}
+decode_hello({hello, _Attrs, Hello}) ->
+ U = make_ref(),
+ try
+ [{'session-id', _, [SessionId]}, _ | _]
+ = [find('session-id', Hello), no_session_id_found | U],
+ [{ok, Id}, _ | _]
+ = [catch {ok, list_to_integer(SessionId)}, invalid_session_id | U],
+ [true, _ | _]
+ = [0 < Id, invalid_session_id | U],
+ [{capabilities, _, Capabilities}, _ | _]
+ = [find(capabilities, Hello), capabilities_not_found | U],
+ [{ok, Caps}, _ | _]
+ = [decode_caps(Capabilities, [], false), false | U],
+ {ok, Id, Caps}
+ catch
+ error: {badmatch, [Error, false | U]} ->
+ Error;
+ error: {badmatch, [_, Reason | U]} ->
+ {error, {incorrect_hello, Reason}}
end.
-decode_caps([{capability,[],[?NETCONF_BASE_CAP++Vsn=Cap]} |Caps], Acc, _) ->
- case Vsn of
- ?NETCONF_BASE_CAP_VSN ->
- decode_caps(Caps, [Cap|Acc], true);
- _ ->
- {error,{incompatible_base_capability_vsn,Vsn}}
- end;
-decode_caps([{capability,[],[Cap]}|Caps],Acc,Base) ->
- decode_caps(Caps,[Cap|Acc],Base);
-decode_caps([H|_T],_,_) ->
- {error,{unexpected_capability_element,H}};
-decode_caps([],_,false) ->
- {error,{incorrect_hello,no_base_capability_found}};
-decode_caps([],Acc,true) ->
- {ok,lists:reverse(Acc)}.
+find(Key, List) ->
+ lists:keyfind(Key, 1, List).
+
+decode_caps([{capability, [], [?NETCONF_BASE_CAP ++ _ = Cap]} | Caps],
+ Acc,
+ _) ->
+ decode_caps(Caps, [Cap|Acc], true);
+decode_caps([{capability, [], [Cap]} | Caps], Acc, Base) ->
+ decode_caps(Caps, [Cap|Acc], Base);
+decode_caps([H|_], _, _) ->
+ {error, {unexpected_capability_element, H}};
+decode_caps([], _, false) ->
+ {error, {incorrect_hello, no_base_capability_found}};
+decode_caps([], Acc, true) ->
+ {ok, lists:reverse(Acc)}.
%% Return a list of {Name,Data}, where data is a {Tag,Value} list for each stream
@@ -1570,7 +1822,7 @@ decode_streams({ok,[{netconf,_,Streams}]}) ->
decode_streams([{streams,_,Streams}]) ->
decode_streams(Streams);
decode_streams([{stream,_,Stream} | Streams]) ->
- {name,_,[Name]} = lists:keyfind(name,1,Stream),
+ {name,_,[Name]} = find(name, Stream),
[{Name,[{Tag,Value} || {Tag,_,[Value]} <- Stream, Tag /= name]}
| decode_streams(Streams)];
decode_streams([]) ->
@@ -1814,6 +2066,190 @@ ssh_close(Connection=#connection{reference = CM}) ->
log(Connection,disconnect),
ok.
+%% ===========================================================================
+
+%% recv/1
+%%
+%% Extract incoming messages using either NETCONF 1.0 framing or
+%% NETCONF 1.1 chunking.
+
+recv(Bin, true) ->
+ recv(Bin, [<<>>, 3]);
+recv(Bin, false) ->
+ recv(Bin, <<>>);
+
+recv(Bin, [Head, Len | Chunks]) -> %% 1.1 chunking
+ chunk(<<Head/binary, Bin/binary>>, Chunks, Len);
+
+%% Start looking for the terminating end-of-message sequence ]]>]]>
+%% 5 characters from the end of the buffered head, since this binary
+%% has already been scanned.
+recv(Bin, Head) when is_binary(Head) -> %% 1.0 framing
+ frame(<<Head/binary, Bin/binary>>, max(0, size(Head) - 5)).
+
+%% frame/2
+%%
+%% Extract a message terminated by the ]]>]]> end-of-message sequence.
+%% Don't need to extract characters as UTF-8 since matching byte-wise
+%% is unambiguous: the high-order bit of every byte of a multi-byte
+%% UTF character is 1, while the end-of-message sequence is ASCII.
+
+frame(Bin, Start) ->
+ Sz = size(Bin),
+ Scope = {Start, Sz - Start},
+ case binary:match(Bin, pattern(), [{scope, Scope}]) of
+ {Len, 6} ->
+ <<Msg:Len/binary, _:6/binary, Rest/binary>> = Bin,
+ {trim(Msg), Rest};
+ nomatch ->
+ Bin
+ end.
+
+%% pattern/0
+
+pattern() ->
+ Key = ?KEY(pattern),
+ case get(Key) of
+ undefined ->
+ CP = binary:compile_pattern(<<"]]>]]>">>),
+ put(Key, CP),
+ CP;
+ CP ->
+ CP
+ end.
+
+%% trim/1
+%%
+%% Whitespace before an XML declaration is an error, but be somewhat
+%% lenient and strip line breaks since the RFC's are unclear on what's
+%% allowed following a ]]>]]> delimiter. Typical seems to be a single
+%% $\n, but strip any of " \t\r\n", and regardless of NETCONF version.
+
+trim(<<C, Bin/binary>>)
+ when C == $\n;
+ C == $\r;
+ C == $\t;
+ C == $ ->
+ trim(Bin);
+
+trim(Bin) ->
+ Bin.
+
+%% chunk/3
+%%
+%% The final argument is either 0 to indicate that a specified number
+%% of bytes of chunk data should be consumed, or at least 3 to
+%% indicate an offset at which to look for a newline following a chunk
+%% size.
+
+%% Accumulating chunk-data ...
+chunk(Bin, [Sz | Chunks] = L, 0) ->
+ case Bin of
+ <<Chunk:Sz/binary, Rest/binary>> ->
+ chunk(Rest, acc(Chunk, Chunks), 3); %% complete chunk ...
+ _ ->
+ [Bin, 0 | L] %% ... or not
+ end;
+
+%% ... or a header.
+
+chunk(Bin, Chunks, Len)
+ when size(Bin) < 4 ->
+ [Bin, 3 = Len | Chunks];
+
+%% End of chunks.
+chunk(<<"\n##\n", Rest/binary>>, Chunks, _) ->
+ case Chunks of
+ [] ->
+ {error, "end-of-chunks unexpected"}; %% must be at least one
+ Bins ->
+ {lists:reverse(Bins), Rest}
+ end;
+
+%% Matching each of the 10 newline possibilities is faster than
+%% searching.
+chunk(<<"\n#", Head:1/binary, $\n, Rest/binary>>, Chunks, _) ->
+ acc(Head, Rest, Chunks);
+chunk(<<"\n#", Head:2/binary, $\n, Rest/binary>>, Chunks, _) ->
+ acc(Head, Rest, Chunks);
+chunk(<<"\n#", Head:3/binary, $\n, Rest/binary>>, Chunks, _) ->
+ acc(Head, Rest, Chunks);
+chunk(<<"\n#", Head:4/binary, $\n, Rest/binary>>, Chunks, _) ->
+ acc(Head, Rest, Chunks);
+chunk(<<"\n#", Head:5/binary, $\n, Rest/binary>>, Chunks, _) ->
+ acc(Head, Rest, Chunks);
+chunk(<<"\n#", Head:6/binary, $\n, Rest/binary>>, Chunks, _) ->
+ acc(Head, Rest, Chunks);
+chunk(<<"\n#", Head:7/binary, $\n, Rest/binary>>, Chunks, _) ->
+ acc(Head, Rest, Chunks);
+chunk(<<"\n#", Head:8/binary, $\n, Rest/binary>>, Chunks, _) ->
+ acc(Head, Rest, Chunks);
+chunk(<<"\n#", Head:9/binary, $\n, Rest/binary>>, Chunks, _) ->
+ acc(Head, Rest, Chunks);
+chunk(<<"\n#", Head:10/binary, $\n, Rest/binary>>, Chunks, _) ->
+ acc(Head, Rest, Chunks);
+
+chunk(<<"\n#", Bin:11/binary, _/binary>>, _, _) ->
+ {error, {"chunk-size too long", Bin}}; %% 32-bits = max 10 digits
+
+chunk(<<"\n#", _/binary>> = Bin, Chunks, _) ->
+ [Bin, size(Bin) | Chunks];
+
+chunk(Bin, Chunks, 3 = Len) ->
+ case drop(Bin) of
+ <<>> ->
+ [Bin, Len | Chunks];
+ <<"\n#", _/binary>> = B ->
+ chunk(B, Chunks, Len);
+ _ ->
+ {error, {"not a chunk", Bin}}
+ end.
+
+%% drop/1
+
+drop(<<"\n#", _/binary>> = Bin) ->
+ Bin;
+
+drop(<<C, Bin/binary>>)
+ when C == $\n;
+ C == $\r;
+ C == $\t;
+ C == $ ->
+ drop(Bin);
+
+drop(Bin) ->
+ Bin.
+
+%% acc/2
+
+acc(Chunk, []) ->
+ [B || B <- [trim(Chunk)], <<>> /= B];
+
+acc(Chunk, Chunks) ->
+ [Chunk | Chunks].
+
+%% acc/3
+
+acc(Head, Rest, Chunks) ->
+ case chunk_size(Head) of
+ {error, _Reason} = No ->
+ No;
+ Sz ->
+ chunk(Rest, [Sz | Chunks], 0)
+ end.
+
+%% chunk_size/1
+
+chunk_size(<<C, _/binary>> = Bin) ->
+ try true = $0 < C, binary_to_integer(Bin) of
+ Sz when 0 < Sz bsr 32 ->
+ {error, {"chunk-size too large", Sz}};
+ Sz ->
+ Sz
+ catch
+ error: _ ->
+ {error, {"chunk-size invalid", Bin}}
+ end.
%%----------------------------------------------------------------------
%% END OF MODULE