Skip to content

Commit

Permalink
add support for the <get-data> command from RFC 8526 (#34)
Browse files Browse the repository at this point in the history
Closes #32
  • Loading branch information
kwatsen authored Oct 16, 2023
1 parent 280d9d6 commit 9ef7326
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 0 deletions.
61 changes: 61 additions & 0 deletions netconf_client/ncclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
edit_config,
get,
get_config,
get_data,
copy_config,
discard_changes,
commit,
Expand Down Expand Up @@ -320,6 +321,62 @@ def get_config(self, source="running", filter=None, with_defaults=None):
(raw, ele) = self._send_rpc(rpc_xml)
return DataReply(raw, ele)

def get_data(
self,
datastore="ds:operational",
filter=None,
config_filter=None,
origin_filters=[],
negate_origin_filters=False,
max_depth=None,
with_origin=False,
with_defaults=None,
):
"""Send a ``<get-data>`` request
:param str datastore: The datastore to retrieve the data from.
:param str filter: Either the ``<subtree-filter>`` or the
``xpath-filter`` node to use in the request.
:param bool config_filter: Specifies if only "config true" or only
"config false" nodes are returned. Both
are returned if unspecified.
:param dict origin_filters: A list of origins (e.g., "or:intended").
No origin filters are applied if unspecified.
:param bool negate_origin_filters: Specifies if origin_filters are negated.
:param int max_depth: A 16-bit unsigned integer. If unspecified,
the max-depth is unbounded.
:param bool with_origin: Specifies if the 'origin' annotation
should be returned for nodes having one.
:param str with_defaults: Specify the mode of default
reporting. See :rfc:`6243`. Can be
``None`` (i.e., omit the
with-defaults tag in the request),
'report-all', 'report-all-tagged',
'trim', or 'explicit'.
:rtype: :class:`DataReply`
"""
rpc_xml = get_data(
datastore=datastore,
filter=filter,
config_filter=config_filter,
origin_filters=origin_filters,
negate_origin_filters=negate_origin_filters,
max_depth=max_depth,
with_origin=with_origin,
with_defaults=with_defaults,
)
(raw, ele) = self._send_rpc(rpc_xml)
return DataReply(raw, ele)

def copy_config(self, target, source, with_defaults=None):
"""Send a ``<copy-config>`` request
Expand Down Expand Up @@ -491,6 +548,10 @@ class DataReply:

def __init__(self, raw, ele):
self.data_ele = ele.find("{urn:ietf:params:xml:ns:netconf:base:1.0}data")
if self.data_ele is None:
self.data_ele = ele.find(
"{urn:ietf:params:xml:ns:yang:ietf-netconf-nmda}data"
)
self.data_xml = etree.tostring(self.data_ele)
self.raw_reply = raw

Expand Down
38 changes: 38 additions & 0 deletions netconf_client/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,44 @@ def get_config(source="running", filter=None, with_defaults=None, msg_id=None):
return make_rpc("".join(pieces), msg_id=msg_id)


def get_data(
datastore="ds:operational",
filter=None,
config_filter=None,
origin_filters=[],
negate_origin_filters=False,
max_depth=None,
with_origin=False,
with_defaults=None,
msg_id=None,
):
pieces = []
pieces.append(
'<get-data xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-nmda" '
+ 'xmlns:ds="urn:ietf:params:xml:ns:yang:ietf-datastores" '
+ 'xmlns:or="urn:ietf:params:xml:ns:yang:ietf-origin">'
)
pieces.append("<datastore>{}</datastore>".format(datastore))
if filter:
pieces.append(filter)
if config_filter is not None:
if config_filter == True:
pieces.append("<config-filter>true</config-filter>")
else:
pieces.append("<config-filter>false</config-filter>")
for origin in origin_filters:
tag = "negated-origin-filter" if negate_origin_filters else "origin-filter"
pieces.append("<{}>{}</{}>".format(tag, origin, tag))
if max_depth:
pieces.append("<max-depth>{}</max-depth>".format(max_depth))
if with_origin:
pieces.append("<with-origin/>")
if with_defaults:
pieces.append(make_with_defaults(with_defaults))
pieces.append("</get-data>")
return make_rpc("".join(pieces), msg_id=msg_id)


def copy_config(target, source, filter=None, with_defaults=None, msg_id=None):
pieces = []
pieces.append("<copy-config>")
Expand Down
118 changes: 118 additions & 0 deletions test/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,124 @@ def test_get(fake_id, log_id, log_local_ip, log_peer_ip, log_content):
assert log_recorder.check_content("get", log_content)


@pytest.mark.parametrize(
"log_id,log_local_ip,log_peer_ip,log_content",
[
# no IDs
(
None,
None,
None,
[
[
"NC Request:\n",
"<get-data ",
"<subtree-filter>foo</subtree-filter>",
"</get-data>",
],
[
r"NC Response \(\d+\.\d+ sec\):\n",
"<rpc-reply ",
"<data>bar</data>",
"</rpc-reply>",
],
],
),
# log ID only
(
"Raspi-4",
None,
None,
[
[
"NC Request => Raspi-4:\n",
"<get-data ",
"<subtree-filter>foo</subtree-filter>",
"</get-data>",
],
[
r"NC Response <= Raspi-4 \(\d+\.\d+ sec\):\n",
"<rpc-reply ",
"<data>bar</data>",
"</rpc-reply>",
],
],
),
# connection IP addresses only
(
None,
"1.2.3.4",
"5.6.7.8",
[
[
r"NC Request 1\.2\.3\.4 => 5\.6\.7\.8:\n",
"<get-data ",
"<subtree-filter>foo</subtree-filter>",
"</get-data>",
],
[
r"NC Response 1\.2\.3\.4 <= 5\.6\.7\.8 \(\d+\.\d+ sec\):\n",
"<rpc-reply ",
"<data>bar</data>",
"</rpc-reply>",
],
],
),
# log ID and connection IP addresses
(
"Raspi-4",
"1.2.3.4",
"5.6.7.8",
[
[
r"NC Request \(1\.2\.3\.4\) => Raspi-4 \(5\.6\.7\.8\):\n",
"<get-data ",
"<subtree-filter>foo</subtree-filter>",
"</get-data>",
],
[
r"NC Response \(1\.2\.3\.4\) <= Raspi-4 \(5\.6\.7\.8\) \(\d+\.\d+ sec\):\n",
"<rpc-reply ",
"<data>bar</data>",
"</rpc-reply>",
],
],
),
],
ids=["no IDs", "w/ log ID", "w/ IP addr", "w/ log ID+IP addr"],
)
def test_get_data(fake_id, log_id, log_local_ip, log_peer_ip, log_content):
with LogSentry(True), MockSession(
[], log_local_ip, log_peer_ip
) as session, Manager(session, timeout=1, log_id=log_id) as mgr:
session.replies.append((RPC_REPLY_DATA, etree.fromstring(RPC_REPLY_DATA)))
r = mgr.get_data(
filter="<subtree-filter>foo</subtree-filter>",
config_filter=True,
origin_filters=["or:system", "or:default"],
negate_origin_filters=True,
with_defaults="explicit",
)
assert session.sent[0] == uglify(
"""
<rpc message-id="fake-id" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<get-data xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-nmda" xmlns:ds="urn:ietf:params:xml:ns:yang:ietf-datastores" xmlns:or="urn:ietf:params:xml:ns:yang:ietf-origin">
<datastore>ds:operational</datastore>
<subtree-filter>foo</subtree-filter>
<config-filter>true</config-filter>
<negated-origin-filter>or:system</negated-origin-filter>
<negated-origin-filter>or:default</negated-origin-filter>
<with-defaults xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults">
explicit
</with-defaults>
</get-data>
</rpc>
"""
)
assert r.data_ele.text == "bar"
assert log_recorder.check_content("get_data", log_content)


def test_xml_error(fake_id):
with LogSentry(True), MockSession([]) as session, Manager(
session, timeout=1
Expand Down

0 comments on commit 9ef7326

Please sign in to comment.