Revision 19e7c7f6
b/docs/source/capabilities.rst | ||
---|---|---|
6 | 6 |
.. autofunction:: schemes |
7 | 7 |
|
8 | 8 |
.. autoclass:: Capabilities |
9 |
:members: |
|
9 |
|
|
10 |
:members: |
|
11 |
|
|
12 |
.. describe:: ":cap" in caps |
|
13 |
|
|
14 |
Check for the presence of capability. In addition to the URI, for capabilities of the form `urn:ietf:params:netconf:capability:$name:$version` their shorthand can be used as a key. For example, for `urn:ietf:params:netconf:capability:candidate:1.0` the shorthand would be `:candidate`. If version is significant, use `:candidate:1.0` as key. |
|
15 |
|
|
16 |
.. describe:: iter(caps) |
|
17 |
|
|
18 |
Return an iterator over the full URI's of capabilities represented by this object. |
b/docs/source/conf.py | ||
---|---|---|
67 | 67 |
exclude_trees = [] |
68 | 68 |
|
69 | 69 |
# The reST default role (used for this markup: `text`) to use for all documents. |
70 |
default_role = 'class'
|
|
70 |
#default_role = 'obj'
|
|
71 | 71 |
|
72 | 72 |
# If true, '()' will be appended to :func: etc. cross-reference text. |
73 | 73 |
#add_function_parentheses = True |
b/docs/source/index.rst | ||
---|---|---|
1 | 1 |
Welcome |
2 | 2 |
======= |
3 | 3 |
|
4 |
``ncclient`` is a Python library for NETCONF clients. It aims to offer an intuitive API that |
|
5 |
sensibly maps the XML-encoded nature of NETCONF to Python constructs and idioms, and make writing |
|
6 |
network-management scripts easier. Other key features are: |
|
4 |
`ncclient` is a Python library for NETCONF clients. It aims to offer an intuitive API that sensibly maps the XML-encoded nature of NETCONF to Python constructs and idioms, and make writing network-management scripts easier. Other key features are: |
|
7 | 5 |
|
8 | 6 |
* Supports all operations and capabilities defined in :rfc:`4741`. |
9 | 7 |
* Request pipelining. |
... | ... | |
11 | 9 |
* Keeping XML out of the way unless really needed. |
12 | 10 |
* Extensible. New transport mappings and capabilities/operations can be easily added. |
13 | 11 |
|
14 |
It is suitable for Python 2.6+ (not Python 3 yet, though), and depends on `paramiko |
|
15 |
<http://www.lag.net/paramiko/>`_, an SSH library. |
|
12 |
It is suitable for Python 2.6+ (not Python 3 yet, though), and depends on `paramiko <http://www.lag.net/paramiko/>`_, an SSH library. |
|
16 | 13 |
|
17 |
The best way to introduce is of course, through a simple code example::
|
|
14 |
The best way to introduce is through a simple code example:: |
|
18 | 15 |
|
19 | 16 |
from ncclient import manager |
20 | 17 |
|
... | ... | |
31 | 28 |
|
32 | 29 |
manager |
33 | 30 |
api |
34 |
extending |
|
35 | 31 |
|
36 | 32 |
Indices and tables |
37 | 33 |
------------------ |
b/docs/source/manager.rst | ||
---|---|---|
4 | 4 |
.. automodule:: ncclient.manager |
5 | 5 |
:synopsis: High-level API |
6 | 6 |
|
7 |
Module data |
|
8 |
----------- |
|
9 | 7 |
|
10 |
These attributes control what capabilties are exchanged with the NETCONF server and what operations |
|
11 |
are available through the `Manager` API. |
|
8 |
Customizing |
|
9 |
------------ |
|
10 |
|
|
11 |
These attributes control what capabilties are exchanged with the NETCONF server and what operations are available through the :class:`Manager` API. |
|
12 | 12 |
|
13 | 13 |
.. autodata:: OPERATIONS |
14 | 14 |
|
15 | 15 |
.. autodata:: CAPABILITIES |
16 | 16 |
|
17 |
|
|
17 | 18 |
Factory functions |
18 | 19 |
----------------- |
19 | 20 |
|
20 |
A `Manager` instance is created using a factory function. |
|
21 |
A :class:`Manager` instance is created using a factory function.
|
|
21 | 22 |
|
22 |
.. autofunction:: connect_ssh(host[, port=830, timeout=None, unknown_host_cb=default_unknown_host_cb, username=None, password, key_filename=None, allow_agent=True, look_for_keys=True])
|
|
23 |
.. autofunction:: connect_ssh |
|
23 | 24 |
|
24 | 25 |
.. autodata:: connect |
25 | 26 |
|
26 | 27 |
Manager |
27 | 28 |
------- |
28 | 29 |
|
29 |
Exposes an API for RPC operations as method calls. The return type of these methods depends on |
|
30 |
whether we are is in :attr:`asynchronous or synchronous mode <ncclient.manager.Manager.async_mode>`. |
|
30 |
Exposes an API for RPC operations as method calls. The return type of these methods depends on whether we are in :attr:`asynchronous or synchronous mode <ncclient.manager.Manager.async_mode>`. |
|
31 | 31 |
|
32 |
In synchronous mode replies are awaited and the corresponding `~ncclient.operations.RPCReply` object |
|
33 |
is returned. Depending on the :attr:`exception raising mode <ncclient.manager.Manager.raise_mode>`, |
|
34 |
an *rpc-error* in the reply may be raised as :exc:`RPCError` exceptions. |
|
32 |
In synchronous mode replies are awaited and the corresponding :class:`~ncclient.operations.RPCReply` object is returned. Depending on the :attr:`exception raising mode <ncclient.manager.Manager.raise_mode>`, an `rpc-error` in the reply may be raised as an :exc:`~ncclient.operations.RPCError` exception. |
|
35 | 33 |
|
36 |
However in asynchronous mode, operations return immediately with an `~ncclient.operations.RPC` |
|
37 |
object. Error handling and checking for whether a reply has been received must be dealt with |
|
38 |
manually. See the `~ncclient.operations.RPC` documentation for details. |
|
34 |
However in asynchronous mode, operations return immediately with the corresponding :class:`~ncclient.operations.RPC` object. Error handling and checking for whether a reply has been received must be dealt with manually. See the :class:`~ncclient.operations.RPC` documentation for details. |
|
39 | 35 |
|
40 |
Note that in case of the *get* and *get-config* operations, the reply is an instance of |
|
41 |
`~ncclient.operations.GetReply` which exposes the additional attributes |
|
42 |
:attr:`~ncclient.operations.GetReply.data` (as `~xml.etree.ElementTree.Element`) and |
|
43 |
:attr:`~ncclient.operations.GetReply.data_xml` (as `string`), which are of primary interest in case |
|
44 |
of these operations. |
|
36 |
Note that in case of the :meth:`~Manager.get` and :meth:`~Manager.get_config` operations, the reply is an instance of :class:`~ncclient.operations.GetReply` which exposes the additional attributes :attr:`~ncclient.operations.GetReply.data` (as :class:`~xml.etree.ElementTree.Element`) and :attr:`~ncclient.operations.GetReply.data_xml` (as a string), which are of primary interest in case of these operations. |
|
45 | 37 |
|
46 |
Presence of capabilities is verified to the extent possible, and you can expect a |
|
47 |
:exc:`~ncclient.operations.MissingCapabilityError` if something is amiss. In case of transport-layer |
|
48 |
errors, e.g. unexpected session close, :exc:`~ncclient.transport.TransportError` will be raised. |
|
38 |
Presence of capabilities is verified to the extent possible, and you can expect a :exc:`~ncclient.operations.MissingCapabilityError` if something is amiss. In case of transport-layer errors, e.g. unexpected session close, :exc:`~ncclient.transport.TransportError` will be raised. |
|
49 | 39 |
|
50 |
.. class:: Manager |
|
51 |
|
|
52 |
For details on the expected behavior of the operations and their parameters |
|
53 |
refer to :rfc:`4741`. |
|
40 |
.. autoclass:: Manager |
|
54 | 41 |
|
55 |
Manager instances are also context managers so you can use it like this::
|
|
42 |
.. automethod:: get_config(source, filter=None)
|
|
56 | 43 |
|
57 |
with manager.connect("host") as m: |
|
58 |
# do your stuff |
|
59 |
|
|
60 |
... or like this:: |
|
61 |
|
|
62 |
m = manager.connect("host") |
|
63 |
try: |
|
64 |
# do your stuff |
|
65 |
finally: |
|
66 |
m.close() |
|
67 |
|
|
68 |
.. method:: get_config(source[, filter=None]) |
|
69 |
|
|
70 |
Retrieve all or part of a specified configuration. |
|
71 |
|
|
72 |
:param source: name of the configuration datastore being queried |
|
73 |
:type source: `string` |
|
74 |
|
|
75 |
:param filter: portions of the device configuration to retrieve (by default entire configuration is retrieved) |
|
76 |
:type filter: :ref:`filter_params` |
|
77 |
|
|
78 |
.. method:: edit_config(target, config[, default_operation=None, test_option=None, error_option=None]) |
|
79 |
|
|
80 |
Loads all or part of a specified configuration to the specified target configuration. |
|
81 |
|
|
82 |
The ``"rollback-on-error"`` *error_option* depends on the ``:rollback-on-error`` capability. |
|
83 |
|
|
84 |
:param target: name of the configuration datastore being edited |
|
85 |
:type target: `string` |
|
86 |
|
|
87 |
:param config: configuration (must be rooted in *<config> .. </config>*) |
|
88 |
:type config: `string` or `~xml.etree.ElementTree.Element` |
|
89 |
|
|
90 |
:param default_operation: one of { ``"merge"``, ``"replace"``, or ``"none"`` } |
|
91 |
:type default_operation: `string` |
|
92 |
|
|
93 |
:param test_option: one of { ``"test_then_set"``, ``"set"`` } |
|
94 |
:type test_option: `string` |
|
95 |
|
|
96 |
:param error_option: one of { ``"stop-on-error"``, ``"continue-on-error"``, ``"rollback-on-error"`` } |
|
97 |
:type error_option: `string` |
|
98 |
|
|
99 |
.. method:: copy_config(source, target) |
|
100 |
|
|
101 |
Create or replace an entire configuration datastore with the contents of another complete |
|
102 |
configuration datastore. |
|
103 |
|
|
104 |
:param source: configuration datastore to use as the source of the copy operation or *<config>* element containing the configuration subtree to copy |
|
105 |
:type source: :ref:`srctarget_params` |
|
106 |
|
|
107 |
:param target: configuration datastore to use as the destination of the copy operation |
|
108 |
:type target: :ref:`srctarget_params` |
|
109 |
|
|
110 |
.. method:: delete_config(target) |
|
111 |
|
|
112 |
Delete a configuration datastore. |
|
113 |
|
|
114 |
:param target: name or URL of configuration datastore to delete |
|
115 |
:type: :ref:`srctarget_params` |
|
116 |
|
|
117 |
.. method:: lock(target) |
|
118 |
|
|
119 |
Allows the client to lock the configuration system of a device. |
|
120 |
|
|
121 |
:param target: name of the configuration datastore to lock |
|
122 |
:type target: `string` |
|
123 |
|
|
124 |
.. method:: unlock(target) |
|
125 |
|
|
126 |
Release a configuration lock, previously obtained with the |
|
127 |
:meth:`~ncclient.manager.Manager.lock` operation. |
|
128 |
|
|
129 |
:param target: name of the configuration datastore to unlock |
|
130 |
:type target: `string` |
|
131 |
|
|
132 |
.. method:: locked(target) |
|
133 |
|
|
134 |
Returns a context manager for a lock on a datastore, e.g.:: |
|
135 |
|
|
136 |
with m.locked("running"): |
|
137 |
# do your stuff |
|
44 |
.. automethod:: edit_config(target, config, default_operation=None, test_option=None, error_option=None) |
|
138 | 45 |
|
139 |
... instead of:: |
|
140 |
|
|
141 |
m.lock("running") |
|
142 |
try: |
|
143 |
# do your stuff |
|
144 |
finally: |
|
145 |
m.unlock("running") |
|
146 |
|
|
147 |
:param target: name of configuration datastore to lock |
|
148 |
:type target: `string` |
|
149 |
|
|
150 |
:rtype: `~ncclient.operations.LockContext` |
|
151 |
|
|
152 |
.. method:: get([filter=None]) |
|
153 |
|
|
154 |
Retrieve running configuration and device state information. |
|
155 |
|
|
156 |
:param filter: portions of the device configuration to retrieve (by default entire configuration is retrieved) |
|
157 |
:type filter: :ref:`filter_params` |
|
158 |
|
|
159 |
.. method:: close_session() |
|
160 |
|
|
161 |
Request graceful termination of the NETCONF session, and also close the transport. |
|
162 |
|
|
163 |
.. method:: kill_session(session_id) |
|
164 |
|
|
165 |
Force the termination of a NETCONF session (not the current one!). |
|
166 |
|
|
167 |
:param session_id: session identifier of the NETCONF session to be terminated |
|
168 |
:type session_id: `string` |
|
169 |
|
|
170 |
.. method:: commit([confirmed=False, timeout=None]) |
|
171 |
|
|
172 |
Commit the candidate configuration as the device's new current configuration. Depends on the |
|
173 |
*:candidate* capability. |
|
174 |
|
|
175 |
A confirmed commit (i.e. if *confirmed* is :const:`True`) is reverted if there is no |
|
176 |
followup commit within the *timeout* interval. If no timeout is specified the confirm |
|
177 |
timeout defaults to 600 seconds (10 minutes). A confirming commit may have the *confirmed* |
|
178 |
parameter but this is not required. Depends on the *:confirmed-commit* capability. |
|
179 |
|
|
180 |
:param confirmed: whether this is a confirmed commit |
|
181 |
:type confirmed: `bool` |
|
182 |
|
|
183 |
:param timeout: confirm timeout in seconds |
|
184 |
:type timeout: `int` |
|
185 |
|
|
186 |
.. method:: discard_changes() |
|
187 |
|
|
188 |
Revert the candidate configuration to the currently running configuration. Any uncommitted |
|
189 |
changes are discarded. |
|
190 |
|
|
191 |
.. method:: validate(source) |
|
192 |
|
|
193 |
Validate the contents of the specified configuration. |
|
194 |
|
|
195 |
:param source: name of the configuration datastore being validated or *<config>* element containing the configuration subtree to be validated |
|
196 |
:type source: :ref:`srctarget_params` |
|
197 |
|
|
198 |
.. attribute:: async_mode |
|
199 |
|
|
200 |
Specify whether operations are executed asynchronously (:const:`True`) |
|
201 |
or synchronously (:const:`False`) (the default). |
|
202 |
|
|
203 |
.. attribute:: raise_mode |
|
204 |
|
|
205 |
Specify which errors are raised as :exc:`~ncclient.operations.RPCError` exceptions. |
|
206 |
Valid values: |
|
207 |
|
|
208 |
* ``"all"`` -- any kind of *rpc-error* (error or warning) |
|
209 |
* ``"errors"`` -- where the *error-type* element says it is an error |
|
210 |
* ``"none"`` -- neither |
|
46 |
.. automethod:: copy_config(source, target) |
|
47 |
|
|
48 |
.. automethod:: delete_config(target) |
|
49 |
|
|
50 |
.. automethod:: lock(target) |
|
211 | 51 |
|
212 |
.. attribute:: client_capabilities |
|
213 |
|
|
214 |
`~ncclient.capabilities.Capabilities` object representing the client's capabilities. |
|
215 |
|
|
216 |
.. attribute:: server_capabilities |
|
217 |
|
|
218 |
`~ncclient.capabilities.Capabilities` object representing the server's capabilities. |
|
219 |
|
|
220 |
.. attribute:: session_id |
|
221 |
|
|
222 |
*session-id* assigned by the NETCONF server. |
|
52 |
.. automethod:: unlock(target) |
|
53 |
|
|
54 |
.. automethod:: locked(target) |
|
55 |
|
|
56 |
.. automethod:: get() |
|
223 | 57 |
|
224 |
.. attribute:: connected |
|
225 |
|
|
226 |
Bolean value indicating whether currently connected to the NETCONF server. |
|
58 |
.. automethod:: close_session() |
|
59 |
|
|
60 |
.. automethod:: kill_session(session_id) |
|
61 |
|
|
62 |
.. automethod:: commit(confirmed=False, timeout=None) |
|
63 |
|
|
64 |
.. automethod:: discard_changes() |
|
65 |
|
|
66 |
.. automethod:: validate(source) |
|
67 |
|
|
68 |
.. autoattribute:: async_mode |
|
69 |
|
|
70 |
.. autoattribute:: raise_mode |
|
71 |
|
|
72 |
.. autoattribute:: client_capabilities |
|
73 |
|
|
74 |
.. autoattribute:: server_capabilities |
|
75 |
|
|
76 |
.. autoattribute:: session_id |
|
77 |
|
|
78 |
.. autoattribute:: connected |
|
227 | 79 |
|
228 | 80 |
|
229 | 81 |
Special kinds of parameters |
... | ... | |
236 | 88 |
Source and target parameters |
237 | 89 |
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
238 | 90 |
|
239 |
Where an method takes a *source* or *target* argument, usually a datastore name or URL is expected. |
|
240 |
The latter depends on the ``:url`` capability and on whether the specific URL scheme is supported. |
|
241 |
Either must be specified as a `string`. For example, ``"running"``, |
|
242 |
``"ftp://user:pass@host/config"``. |
|
91 |
Where an method takes a *source* or *target* argument, usually a datastore name or URL is expected. The latter depends on the `:url` capability and on whether the specific URL scheme is supported. Either must be specified as a string. For example, `"running"`, `"ftp://user:pass@host/config"`. |
|
243 | 92 |
|
244 |
If the source may be a *<config>* element, e.g. as allowed for the *validate* RPC, it can also be |
|
245 |
specified as an XML string or an `~xml.etree.ElementTree.Element` object. |
|
93 |
If the source may be a `config` element, e.g. as allowed for the `validate` RPC, it can also be specified as an XML string or an :class:`~xml.etree.ElementTree.Element` object. |
|
246 | 94 |
|
247 | 95 |
.. _filter_params: |
248 | 96 |
|
... | ... | |
251 | 99 |
|
252 | 100 |
Where a method takes a *filter* argument, it can take on the following types: |
253 | 101 |
|
254 |
* A ``tuple`` of *(type, criteria)*.
|
|
102 |
* A tuple of *(type, criteria)*.
|
|
255 | 103 |
|
256 |
Here *type* has to be one of ``"xpath"`` or ``"subtree"``.
|
|
104 |
Here *type* has to be one of `"xpath"` or `"subtree"`.
|
|
257 | 105 |
|
258 |
* For ``"xpath"`` the *criteria* should be a `string` containing the XPath expression. |
|
259 |
* For ``"subtree"`` the *criteria* should be an XML string or an |
|
260 |
`~xml.etree.ElementTree.Element` object containing the criteria. |
|
106 |
* For `"xpath"` the *criteria* should be a string containing the XPath expression. |
|
107 |
* For `"subtree"` the *criteria* should be an XML string or an :class:`~xml.etree.ElementTree.Element` object containing the criteria. |
|
261 | 108 |
|
262 |
* A *<filter>* element as an XML string or an `~xml.etree.ElementTree.Element` object. |
|
109 |
* A `<filter>` element as an XML string or an :class:`~xml.etree.ElementTree.Element` object. |
b/docs/source/operations.rst | ||
---|---|---|
1 | 1 |
:mod:`~ncclient.operations` -- Everything RPC |
2 | 2 |
============================================= |
3 | 3 |
|
4 |
Base classes |
|
5 |
------------ |
|
6 | 4 |
|
7 | 5 |
.. module:: ncclient.operations |
8 | 6 |
:synopsis: Everything RPC |
9 | 7 |
|
10 |
.. autoclass:: RPC(session[, async=False, timeout=None, raise_mode="none"]) |
|
8 |
.. autoclass:: RaiseMode |
|
9 |
:members: NONE, ERRORS, ALL |
|
10 |
|
|
11 |
Base classes |
|
12 |
------------ |
|
13 |
|
|
14 |
.. autoclass:: RPC(session, async=False, timeout=None, raise_mode="none") |
|
11 | 15 |
:members: DEPENDS, REPLY_CLS, _assert, _request, request, event, error, reply, raise_mode, is_async, timeout |
12 | 16 |
|
13 | 17 |
.. autoclass:: RPCReply |
14 |
:members: xml, ok, error, errors |
|
18 |
:members: xml, ok, error, errors, _parsing_hook
|
|
15 | 19 |
|
16 | 20 |
.. autoexception:: RPCError |
17 | 21 |
:show-inheritance: |
... | ... | |
20 | 24 |
Operations |
21 | 25 |
---------- |
22 | 26 |
|
23 |
*TODO* |
|
24 |
|
|
25 |
The operation classes are currently undocumented. See documentation of `~ncclient.manager.Manager` |
|
26 |
for methods that utilize the operation classes. The parameters accepted by :meth:`~RPC.request` for |
|
27 |
these classes are the same. |
|
27 |
The operation classes are currently undocumented. See documentation of :class:`~ncclient.manager.Manager` for methods that utilize the operation classes. The parameters accepted by :meth:`~RPC.request` for these classes are the same. |
|
28 | 28 |
|
29 | 29 |
Replies with data |
30 | 30 |
----------------- |
b/docs/source/transport.rst | ||
---|---|---|
22 | 22 |
:show-inheritance: |
23 | 23 |
:members: load_known_hosts, close, transport |
24 | 24 |
|
25 |
.. automethod:: connect(host[, port=830, timeout=None, username=None, password=None, key_filename=None, allow_agent=True, look_for_keys=True]) |
|
25 |
.. automethod:: connect(host[, port=830, timeout=None, unknown_host_cb=default_unknown_host_cb, username=None, password=None, key_filename=None, allow_agent=True, look_for_keys=True])
|
|
26 | 26 |
|
27 | 27 |
Errors |
28 | 28 |
------ |
... | ... | |
40 | 40 |
:show-inheritance: |
41 | 41 |
|
42 | 42 |
.. autoexception:: SSHUnknownHostError |
43 |
:show-inheritance: |
|
44 |
|
|
43 |
:show-inheritance: |
b/docs/source/xml_.rst | ||
---|---|---|
1 | 1 |
:mod:`~ncclient.xml_` -- XML handling |
2 | 2 |
===================================== |
3 | 3 |
|
4 |
.. module:: ncclient.xml_ |
|
4 |
.. automodule:: ncclient.xml_
|
|
5 | 5 |
:synopsis: XML handling |
6 | 6 |
|
7 | 7 |
.. autoexception:: XMLError |
... | ... | |
20 | 20 |
|
21 | 21 |
.. autodata:: FLOWMON_1_0 |
22 | 22 |
|
23 |
.. function:: register_namespace(prefix, uri) |
|
24 |
|
|
25 |
ElementTree's namespace map determines the prefixes for namespace URI's when serializing to XML. |
|
26 |
This method allows modifying this map to specify a prefix for a namespace URI. |
|
23 |
.. autofunction:: register_namespace(prefix, uri) |
|
27 | 24 |
|
28 | 25 |
.. autofunction:: qualify |
29 | 26 |
|
... | ... | |
36 | 33 |
|
37 | 34 |
.. autofunction:: parse_root |
38 | 35 |
|
39 |
.. autofunction:: validated_element |
|
40 |
|
|
41 |
.. |
|
36 |
.. autofunction:: validated_element |
b/ncclient/capabilities.py | ||
---|---|---|
23 | 23 |
return [] |
24 | 24 |
|
25 | 25 |
def schemes(url_uri): |
26 |
"""Given a URI that has a *scheme* query string (i.e. *:url* capability URI), will return a list |
|
27 |
of supported schemes. |
|
28 |
""" |
|
26 |
"Given a URI that has a *scheme* query string (i.e. `:url` capability URI), will return a list of supported schemes." |
|
29 | 27 |
return url_uri.partition("?scheme=")[2].split(",") |
30 | 28 |
|
31 | 29 |
class Capabilities: |
32 | 30 |
|
33 |
"""Represents the set of capabilities available to a NETCONF client or server. It is initialized |
|
34 |
with a list of capability URI's. These can be iterated over. |
|
35 |
|
|
36 |
Presence of a capability can be checked with the *in* operation. In addition to the URI, for |
|
37 |
capabilities of the form *urn:ietf:params:netconf:capability:$name:$version* their shorthand can |
|
38 |
be used as a key. For example, for *urn:ietf:params:netconf:capability:candidate:1.0* the |
|
39 |
shorthand would be *:candidate*. If version is significant, use *:candidate:1.0* as key. |
|
40 |
""" |
|
31 |
"Represents the set of capabilities available to a NETCONF client or server. It is initialized with a list of capability URI's." |
|
41 | 32 |
|
42 | 33 |
def __init__(self, capabilities): |
43 | 34 |
self._dict = {} |
b/ncclient/manager.py | ||
---|---|---|
12 | 12 |
# See the License for the specific language governing permissions and |
13 | 13 |
# limitations under the License. |
14 | 14 |
|
15 |
"This module is a thin layer of abstraction around the library. It exposes all core functionality."
|
|
15 |
"""This module is a thin layer of abstraction around the library. It exposes all core functionality."""
|
|
16 | 16 |
|
17 | 17 |
import capabilities |
18 | 18 |
import operations |
... | ... | |
35 | 35 |
"urn:liberouter:params:netconf:capability:power-control:1.0" |
36 | 36 |
"urn:ietf:params:netconf:capability:interleave:1.0" |
37 | 37 |
] |
38 |
"A list of URI's representing the client's capabilities. This is used during the initial capability exchange. Modify this if you need to announce some capability not already included."
|
|
38 |
"""A list of URI's representing the client's capabilities. This is used during the initial capability exchange. Modify this if you need to announce some capability not already included."""
|
|
39 | 39 |
|
40 | 40 |
OPERATIONS = { |
41 | 41 |
"get": operations.Get, |
... | ... | |
53 | 53 |
"poweroff_machine": operations.PoweroffMachine, |
54 | 54 |
"reboot_machine": operations.RebootMachine |
55 | 55 |
} |
56 |
"""Dictionary of method names and corresponding `~ncclient.operations.RPC` subclasses. It is used to lookup operations, e.g. "get_config" is mapped to `~ncclient.operations.GetConfig`. It is thus possible to add additional operations to the `Manager` API."""
|
|
56 |
"""Dictionary of method names and corresponding :class:`~ncclient.operations.RPC` subclasses. It is used to lookup operations, e.g. `get_config` is mapped to :class:`~ncclient.operations.GetConfig`. It is thus possible to add additional operations to the :class:`Manager` API."""
|
|
57 | 57 |
|
58 | 58 |
def connect_ssh(*args, **kwds): |
59 |
"""Initializes a NETCONF session over SSH, and creates a connected `Manager` instance. *host* must be specified, all the other arguments are optional and depend on the kind of host key verification and user authentication you want to complete. |
|
60 |
|
|
61 |
For the purpose of host key verification, on -NIX systems a user's :file:`~/.ssh/known_hosts` file is automatically considered. The *unknown_host_cb* argument specifies a callback that will be invoked when the server's host key cannot be verified. See :func:`~ncclient.transport.ssh.default_unknown_host_cb` for function signature. |
|
62 |
|
|
63 |
First, ``publickey`` authentication is attempted. If a specific *key_filename* is specified, it |
|
64 |
will be loaded and authentication attempted using it. If *allow_agent* is :const:`True` and an |
|
65 |
SSH agent is running, the keys provided by the agent will be tried. If *look_for_keys* is |
|
66 |
:const:`True`, keys in the :file:`~/.ssh/id_rsa` and :file:`~.ssh/id_dsa` will be tried. In case |
|
67 |
an encrypted key file is encountered, the *password* argument will be used as a decryption |
|
68 |
passphrase. |
|
69 |
|
|
70 |
If ``publickey`` authentication fails and the *password* argument has been supplied, ``password`` / ``keyboard-interactive`` SSH authentication will be attempted. |
|
71 |
|
|
72 |
:param host: hostname or address on which to connect |
|
73 |
:type host: `string` |
|
74 |
|
|
75 |
:param port: port on which to connect |
|
76 |
:type port: `int` |
|
77 |
|
|
78 |
:param timeout: timeout for socket connect |
|
79 |
:type timeout: `int` |
|
80 |
|
|
81 |
:param unknown_host_cb: optional; callback that is invoked when host key verification fails |
|
82 |
:type unknown_host_cb: `function` |
|
83 |
|
|
84 |
:param username: username to authenticate with, if not specified the username of the logged-in user is used |
|
85 |
:type username: `string` |
|
86 |
|
|
87 |
:param password: password for ``password`` authentication or passphrase for decrypting private key files |
|
88 |
:type password: `string` |
|
89 |
|
|
90 |
:param key_filename: location of a private key file on the file system |
|
91 |
:type key_filename: `string` |
|
92 |
|
|
93 |
:param allow_agent: whether to try connecting to SSH agent for keys |
|
94 |
:type allow_agent: `bool` |
|
95 |
|
|
96 |
:param look_for_keys: whether to look in usual locations for keys |
|
97 |
:type look_for_keys: `bool` |
|
98 |
|
|
99 |
:raises: :exc:`~ncclient.transport.SSHUnknownHostError` |
|
100 |
:raises: :exc:`~ncclient.transport.AuthenticationError` |
|
101 |
|
|
102 |
:rtype: `Manager` |
|
59 |
"""Initialize a :class:`Manager` over the SSH transport. For documentation of arguments see :meth:`ncclient.transport.SSHSession.connect`. |
|
60 |
|
|
61 |
The underlying :class:`ncclient.transport.SSHSession` is created with :data:`CAPABILITIES`. It is first instructed to :meth:`~ncclient.transport.SSHSession.load_known_hosts` and then all the provided arguments are passed directly to its implementation of :meth:`~ncclient.transport.SSHSession.connect`. |
|
103 | 62 |
""" |
104 | 63 |
session = transport.SSHSession(capabilities.Capabilities(CAPABILITIES)) |
105 | 64 |
session.load_known_hosts() |
... | ... | |
110 | 69 |
"Same as :func:`connect_ssh`, since SSH is the default (and currently, the only) transport." |
111 | 70 |
|
112 | 71 |
class OpExecutor(type): |
72 |
|
|
113 | 73 |
def __new__(cls, name, bases, attrs): |
114 | 74 |
def make_wrapper(op_cls): |
115 | 75 |
def wrapper(self, *args, **kwds): |
... | ... | |
122 | 82 |
|
123 | 83 |
class Manager(object): |
124 | 84 |
|
125 |
__metaclass__ = OpExecutor |
|
85 |
"""For details on the expected behavior of the operations and their parameters refer to :rfc:`4741`. |
|
86 |
|
|
87 |
Manager instances are also context managers so you can use it like this:: |
|
88 |
|
|
89 |
with manager.connect("host") as m: |
|
90 |
# do your stuff |
|
126 | 91 |
|
127 |
RAISE_NONE = 0 |
|
128 |
RAISE_ERRORS = 1 |
|
129 |
RAISE_ALL = 2 |
|
92 |
... or like this:: |
|
130 | 93 |
|
131 |
def __init__(self, session): |
|
94 |
m = manager.connect("host") |
|
95 |
try: |
|
96 |
# do your stuff |
|
97 |
finally: |
|
98 |
m.close_session() |
|
99 |
""" |
|
100 |
|
|
101 |
__metaclass__ = OpExecutor |
|
102 |
|
|
103 |
def __init__(self, session, timeout=30): |
|
132 | 104 |
self._session = session |
133 | 105 |
self._async_mode = False |
134 |
self._timeout = None
|
|
135 |
self._raise_mode = self.RAISE_ALL
|
|
106 |
self._timeout = timeout
|
|
107 |
self._raise_mode = operations.RaiseMode.ALL
|
|
136 | 108 |
|
137 | 109 |
def __enter__(self): |
138 | 110 |
return self |
... | ... | |
141 | 113 |
self.close_session() |
142 | 114 |
return False |
143 | 115 |
|
116 |
def __set_async_mode(self, mode): |
|
117 |
self._async_mode = mode |
|
118 |
|
|
119 |
def __set_raise_mode(self, mode): |
|
120 |
assert(choice in (operations.RaiseMode.NONE, operations.RaiseMode.ERRORS, operations.RaiseMode.ALL)) |
|
121 |
self._raise_mode = mode |
|
122 |
|
|
144 | 123 |
def execute(self, cls, *args, **kwds): |
145 | 124 |
return cls(self._session, |
146 | 125 |
async=self._async_mode, |
... | ... | |
148 | 127 |
raise_mode=self._raise_mode).request(*args, **kwds) |
149 | 128 |
|
150 | 129 |
def locked(self, target): |
130 |
"""Returns a context manager for a lock on a datastore, where *target* is the name of the configuration datastore to lock, e.g.:: |
|
131 |
|
|
132 |
with m.locked("running"): |
|
133 |
# do your stuff |
|
134 |
|
|
135 |
... instead of:: |
|
136 |
|
|
137 |
m.lock("running") |
|
138 |
try: |
|
139 |
# do your stuff |
|
140 |
finally: |
|
141 |
m.unlock("running") |
|
142 |
""" |
|
151 | 143 |
return operations.LockContext(self._session, target) |
152 | 144 |
|
153 | 145 |
@property |
154 | 146 |
def client_capabilities(self): |
147 |
":class:`~ncclient.capabilities.Capabilities` object representing the client's capabilities." |
|
155 | 148 |
return self._session._client_capabilities |
156 | 149 |
|
157 | 150 |
@property |
158 | 151 |
def server_capabilities(self): |
152 |
":class:`~ncclient.capabilities.Capabilities` object representing the server's capabilities." |
|
159 | 153 |
return self._session._server_capabilities |
160 | 154 |
|
161 | 155 |
@property |
162 | 156 |
def session_id(self): |
157 |
"`session-id` assigned by the NETCONF server." |
|
163 | 158 |
return self._session.id |
164 | 159 |
|
165 | 160 |
@property |
166 | 161 |
def connected(self): |
162 |
"Whether currently connected to the NETCONF server." |
|
167 | 163 |
return self._session.connected |
168 | 164 |
|
169 |
def set_async_mode(self, mode): |
|
170 |
self._async_mode = mode |
|
171 |
|
|
172 |
def set_raise_mode(self, mode): |
|
173 |
assert(choice in (self.RAISE_NONE, self.RAISE_ERRORS, self.RAISE_ALL)) |
|
174 |
self._raise_mode = mode |
|
165 |
async_mode = property(fget=lambda self: self._async_mode, fset=__set_async_mode) |
|
166 |
"Specify whether operations are executed asynchronously (`True`) or synchronously (`False`) (the default)." |
|
175 | 167 |
|
176 |
async_mode = property(fget=lambda self: self._async_mode, fset=set_async_mode) |
|
177 | 168 |
|
178 |
raise_mode = property(fget=lambda self: self._raise_mode, fset=set_raise_mode) |
|
169 |
raise_mode = property(fget=lambda self: self._raise_mode, fset=__set_raise_mode) |
|
170 |
"Specify which errors are raised as :exc:`~ncclient.operations.RPCError` exceptions. Valid values are the constants defined in :class:`~ncclient.operations.RaiseMode`. The default value is :attr:`~ncclient.operations.RaiseMode.ALL`." |
b/ncclient/operations/__init__.py | ||
---|---|---|
13 | 13 |
# limitations under the License. |
14 | 14 |
|
15 | 15 |
from errors import OperationError, TimeoutExpiredError, MissingCapabilityError |
16 |
from rpc import RPC, RPCReply, RPCError |
|
16 |
from rpc import RPC, RPCReply, RPCError, RaiseMode
|
|
17 | 17 |
|
18 | 18 |
# rfc4741 ops |
19 | 19 |
from retrieve import Get, GetConfig, GetReply |
... | ... | |
27 | 27 |
'RPC', |
28 | 28 |
'RPCReply', |
29 | 29 |
'RPCError', |
30 |
'RaiseMode', |
|
30 | 31 |
'Get', |
31 | 32 |
'GetConfig', |
32 | 33 |
'GetReply', |
b/ncclient/operations/edit.py | ||
---|---|---|
19 | 19 |
import util |
20 | 20 |
|
21 | 21 |
import logging |
22 |
|
|
22 | 23 |
logger = logging.getLogger("ncclient.operations.edit") |
23 | 24 |
|
24 | 25 |
"Operations related to changing device configuration" |
25 | 26 |
|
26 | 27 |
class EditConfig(RPC): |
28 |
"`edit-config` RPC" |
|
27 | 29 |
|
28 |
"*<edit-config>* RPC" |
|
29 |
|
|
30 | 30 |
def request(self, target, config, default_operation=None, test_option=None, error_option=None): |
31 |
"""Loads all or part of the specified *config* to the *target* configuration datastore. |
|
32 |
|
|
33 |
*target* is the name of the configuration datastore being edited |
|
34 |
|
|
35 |
*config* is the configuration, which must be rooted in the `config` element. It can be specified either as a string or an :class:`~xml.etree.ElementTree.Element`. |
|
36 |
|
|
37 |
*default_operation* if specified must be one of { `"merge"`, `"replace"`, or `"none"` } |
|
38 |
|
|
39 |
*test_option* if specified must be one of { `"test_then_set"`, `"set"` } |
|
40 |
|
|
41 |
*error_option* if specified must be one of { `"stop-on-error"`, `"continue-on-error"`, `"rollback-on-error"` } |
|
42 |
|
|
43 |
The `"rollback-on-error"` *error_option* depends on the `:rollback-on-error` capability. |
|
44 |
""" |
|
31 | 45 |
node = new_ele("edit-config") |
32 | 46 |
node.append(util.datastore_or_url("target", target, self._assert)) |
33 | 47 |
if error_option is not None: |
... | ... | |
38 | 52 |
self._assert(':validate') |
39 | 53 |
sub_ele(node, "test-option").text = test_option |
40 | 54 |
if default_operation is not None: |
41 |
# TODO: check if it is a valid default-operation
|
|
55 |
# TODO: check if it is a valid default-operation |
|
42 | 56 |
sub_ele(node, "default-operation").text = default_operation |
43 | 57 |
node.append(validated_element(config, ("config", qualify("config")))) |
44 | 58 |
return self._request(node) |
45 | 59 |
|
46 | 60 |
|
47 | 61 |
class DeleteConfig(RPC): |
48 |
|
|
49 |
"*<delete-config>* RPC" |
|
62 |
"`delete-config` RPC" |
|
50 | 63 |
|
51 | 64 |
def request(self, target): |
65 |
"""Delete a configuration datastore. |
|
66 |
|
|
67 |
:param target: name or URL of configuration datastore to delete |
|
68 |
:type: :ref:`srctarget_params`""" |
|
52 | 69 |
node = new_ele("delete-config") |
53 | 70 |
node.append(util.datastore_or_url("target", target, self._assert)) |
54 | 71 |
return self._request(node) |
55 | 72 |
|
56 | 73 |
|
57 | 74 |
class CopyConfig(RPC): |
75 |
"`copy-config` RPC" |
|
58 | 76 |
|
59 |
"*<copy-config>* RPC" |
|
60 |
|
|
61 | 77 |
def request(self, source, target): |
78 |
"""Create or replace an entire configuration datastore with the contents of another complete |
|
79 |
configuration datastore. |
|
80 |
|
|
81 |
:param source: configuration datastore to use as the source of the copy operation or `config` element containing the configuration subtree to copy |
|
82 |
:type source: :ref:`srctarget_params` |
|
83 |
|
|
84 |
:param target: configuration datastore to use as the destination of the copy operation |
|
85 |
:type target: :ref:`srctarget_params`""" |
|
62 | 86 |
node = new_ele("copy-config") |
63 | 87 |
node.append(util.datastore_or_url("target", target, self._assert)) |
64 | 88 |
node.append(util.datastore_or_url("source", source, self._assert)) |
... | ... | |
66 | 90 |
|
67 | 91 |
|
68 | 92 |
class Validate(RPC): |
69 |
|
|
70 |
"*<validate>* RPC. Depends on the *:validate* capability." |
|
93 |
"`validate` RPC. Depends on the `:validate` capability." |
|
71 | 94 |
|
72 | 95 |
DEPENDS = [':validate'] |
73 | 96 |
|
74 | 97 |
def request(self, source): |
98 |
"""Validate the contents of the specified configuration. |
|
99 |
|
|
100 |
:param source: name of the configuration datastore being validated or `config` element containing the configuration subtree to be validated |
|
101 |
:type source: :ref:`srctarget_params`""" |
|
75 | 102 |
node = new_ele("validate") |
76 | 103 |
try: |
77 | 104 |
src = validated_element(source, ("config", qualify("config"))) |
... | ... | |
83 | 110 |
|
84 | 111 |
|
85 | 112 |
class Commit(RPC): |
86 |
|
|
87 |
"*<commit>* RPC. Depends on the *:candidate* capability, and the *:confirmed-commit* " |
|
113 |
"`commit` RPC. Depends on the `:candidate` capability, and the `:confirmed-commit`." |
|
88 | 114 |
|
89 | 115 |
DEPENDS = [':candidate'] |
90 |
|
|
116 |
|
|
91 | 117 |
def request(self, confirmed=False, timeout=None): |
118 |
"""Commit the candidate configuration as the device's new current configuration. Depends on the `:candidate` capability. |
|
119 |
|
|
120 |
A confirmed commit (i.e. if *confirmed* is `True`) is reverted if there is no followup commit within the *timeout* interval. If no timeout is specified the confirm timeout defaults to 600 seconds (10 minutes). A confirming commit may have the *confirmed* parameter but this is not required. Depends on the `:confirmed-commit` capability. |
|
121 |
|
|
122 |
:param confirmed: whether this is a confirmed commit |
|
123 |
:type confirmed: bool |
|
124 |
|
|
125 |
:param timeout: confirm timeout in seconds |
|
126 |
:type timeout: int""" |
|
92 | 127 |
node = new_ele("commit") |
93 | 128 |
if confirmed: |
94 | 129 |
self._assert(":confirmed-commit") |
... | ... | |
99 | 134 |
|
100 | 135 |
|
101 | 136 |
class DiscardChanges(RPC): |
102 |
|
|
103 |
"*<discard-changes>* RPC. Depends on the *:candidate* capability." |
|
137 |
"`discard-changes` RPC. Depends on the `:candidate` capability." |
|
104 | 138 |
|
105 | 139 |
DEPENDS = [":candidate"] |
106 | 140 |
|
107 | 141 |
def request(self): |
142 |
"""Revert the candidate configuration to the currently running configuration. Any uncommitted changes are discarded.""" |
|
108 | 143 |
return self._request(new_ele("discard-changes")) |
b/ncclient/operations/lock.py | ||
---|---|---|
12 | 12 |
# See the License for the specific language governing permissions and |
13 | 13 |
# limitations under the License. |
14 | 14 |
|
15 |
'Locking-related NETCONF operations'
|
|
15 |
"Locking-related NETCONF operations"
|
|
16 | 16 |
|
17 | 17 |
from ncclient.xml_ import * |
18 | 18 |
|
19 |
from rpc import RPC |
|
19 |
from rpc import RaiseMode, RPC
|
|
20 | 20 |
|
21 |
# TODO: |
|
22 |
# should have some way to parse session-id from a lock-denied error, and raise |
|
23 |
# a tailored exception |
|
21 |
# TODO: parse session-id from a lock-denied error, and raise a tailored exception? |
|
24 | 22 |
|
25 | 23 |
class Lock(RPC): |
26 | 24 |
|
27 |
"*<lock>* RPC"
|
|
25 |
"`lock` RPC"
|
|
28 | 26 |
|
29 | 27 |
def request(self, target): |
28 |
"""Allows the client to lock the configuration system of a device. |
|
29 |
|
|
30 |
*target* is the name of the configuration datastore to lock |
|
31 |
""" |
|
30 | 32 |
node = new_ele("lock") |
31 | 33 |
sub_ele(sub_ele(node, "target"), target) |
32 | 34 |
return self._request(node) |
... | ... | |
34 | 36 |
|
35 | 37 |
class Unlock(RPC): |
36 | 38 |
|
37 |
"*<unlock>* RPC"
|
|
39 |
"`unlock` RPC"
|
|
38 | 40 |
|
39 | 41 |
def request(self, target): |
42 |
"""Release a configuration lock, previously obtained with the lock operation. |
|
43 |
|
|
44 |
*target* is the name of the configuration datastore to unlock |
|
45 |
""" |
|
40 | 46 |
node = new_ele("unlock") |
41 | 47 |
sub_ele(sub_ele(node, "target"), target) |
42 | 48 |
return self._request(node) |
... | ... | |
44 | 50 |
|
45 | 51 |
class LockContext: |
46 | 52 |
|
47 |
""" |
|
48 |
A context manager for the :class:`Lock` / :class:`Unlock` pair of RPC's. |
|
49 |
|
|
50 |
RPC errors are always raised as exceptions. |
|
51 |
|
|
52 |
Initialise with (:class:`Session <ncclient.transport.Session>`) instance |
|
53 |
and lock target. |
|
53 |
"""A context manager for the :class:`Lock` / :class:`Unlock` pair of RPC's. |
|
54 |
|
|
55 |
Any `rpc-error` will be raised as an exception. |
|
56 |
|
|
57 |
Initialise with (:class:`Session <ncclient.transport.Session>`) instance and lock target. |
|
54 | 58 |
""" |
55 | 59 |
|
56 | 60 |
def __init__(self, session, target): |
... | ... | |
58 | 62 |
self.target = target |
59 | 63 |
|
60 | 64 |
def __enter__(self): |
61 |
Lock(self.session).request(self.target) |
|
65 |
Lock(self.session, raise_mode=RaiseMode.ERRORS).request(self.target)
|
|
62 | 66 |
return self |
63 | 67 |
|
64 | 68 |
def __exit__(self, *args): |
65 |
Unlock(self.session).request(self.target) |
|
69 |
Unlock(self.session, raise_mode=RaiseMode.ERRORS).request(self.target)
|
|
66 | 70 |
return False |
b/ncclient/operations/retrieve.py | ||
---|---|---|
30 | 30 |
|
31 | 31 |
@property |
32 | 32 |
def data_ele(self): |
33 |
"*data* element as an `~xml.etree.ElementTree.Element`" |
|
33 |
"*data* element as an :class:`~xml.etree.ElementTree.Element`"
|
|
34 | 34 |
if not self._parsed: |
35 | 35 |
self.parse() |
36 | 36 |
return self._data |
... | ... | |
53 | 53 |
REPLY_CLS = GetReply |
54 | 54 |
|
55 | 55 |
def request(self, filter=None): |
56 |
"""Retrieve running configuration and device state information. |
|
57 |
|
|
58 |
:param filter: portions of the device configuration to retrieve (by default entire configuration is retrieved) |
|
59 |
:type filter: :ref:`filter_params` |
|
60 |
""" |
|
56 | 61 |
node = new_ele("get") |
57 | 62 |
if filter is not None: |
58 | 63 |
node.append(util.build_filter(filter)) |
... | ... | |
66 | 71 |
REPLY_CLS = GetReply |
67 | 72 |
|
68 | 73 |
def request(self, source, filter=None): |
74 |
"""Retrieve all or part of a specified configuration. |
|
75 |
|
|
76 |
:param source: name of the configuration datastore being queried |
|
77 |
:type source: string |
|
78 |
|
|
79 |
:param filter: portions of the device configuration to retrieve (by default entire configuration is retrieved) |
|
80 |
:type filter: :ref:`filter_params`""" |
|
69 | 81 |
node = new_ele("get-config") |
70 | 82 |
node.append(util.datastore_or_url("source", source, self._assert)) |
71 | 83 |
if filter is not None: |
b/ncclient/operations/rpc.py | ||
---|---|---|
26 | 26 |
|
27 | 27 |
class RPCError(OperationError): |
28 | 28 |
|
29 |
"""Represents an *rpc-error*. It is a type of :exc:`OperationError` and can be raised like any |
|
30 |
other exception.""" |
|
29 |
"Represents an `rpc-error`. It is a type of :exc:`OperationError` and can be raised as such." |
|
31 | 30 |
|
32 | 31 |
tag_to_attr = { |
33 | 32 |
qualify("error-type"): "_type", |
... | ... | |
56 | 55 |
|
57 | 56 |
@property |
58 | 57 |
def xml(self): |
59 |
"*rpc-error* element as returned."
|
|
58 |
"The `rpc-error` element as returned in XML."
|
|
60 | 59 |
return self._raw |
61 | 60 |
|
62 | 61 |
@property |
63 | 62 |
def type(self): |
64 |
"`string` representing text of *error-type* element."
|
|
63 |
"The contents of the `error-type` element."
|
|
65 | 64 |
return self._type |
66 | 65 |
|
67 | 66 |
@property |
68 | 67 |
def tag(self): |
69 |
"`string` representing text of *error-tag* element."
|
|
68 |
"The contents of the `error-tag` element."
|
|
70 | 69 |
return self._tag |
71 | 70 |
|
72 | 71 |
@property |
73 | 72 |
def severity(self): |
74 |
"`string` representing text of *error-severity* element."
|
|
73 |
"The contents of the `error-severity` element."
|
|
75 | 74 |
return self._severity |
76 | 75 |
|
77 | 76 |
@property |
78 | 77 |
def path(self): |
79 |
"`string` or :const:`None`; representing text of *error-path* element."
|
|
78 |
"The contents of the `error-path` element if present or `None`."
|
|
80 | 79 |
return self._path |
81 | 80 |
|
82 | 81 |
@property |
83 | 82 |
def message(self): |
84 |
"`string` or :const:`None`; representing text of *error-message* element."
|
|
83 |
"The contents of the `error-message` element if present or `None`."
|
|
85 | 84 |
return self._message |
86 | 85 |
|
87 | 86 |
@property |
88 | 87 |
def info(self): |
89 |
"`string` (XML) or :const:`None`; representing *error-info* element."
|
|
88 |
"XML string or `None`; representing the `error-info` element."
|
|
90 | 89 |
return self._info |
91 | 90 |
|
92 | 91 |
|
... | ... | |
128 | 127 |
self._parsed = True |
129 | 128 |
|
130 | 129 |
def _parsing_hook(self, root): |
130 |
"No-op by default. Gets given the *root* element." |
|
131 | 131 |
pass |
132 | 132 |
|
133 | 133 |
@property |
... | ... | |
142 | 142 |
|
143 | 143 |
@property |
144 | 144 |
def error(self): |
145 |
"Returns the first `RPCError` and :const:`None` if there were no errors."
|
|
145 |
"Returns the first :class:`RPCError` and `None` if there were no errors."
|
|
146 | 146 |
self.parse() |
147 | 147 |
if self._errors: |
148 | 148 |
return self._errors[0] |
... | ... | |
151 | 151 |
|
152 | 152 |
@property |
153 | 153 |
def errors(self): |
154 |
"""`list` of `RPCError` objects. Will be empty if there were no *rpc-error* elements in |
|
155 |
reply.""" |
|
154 |
"List of `RPCError` objects. Will be empty if there were no *rpc-error* elements in reply." |
|
156 | 155 |
self.parse() |
157 | 156 |
return self._errors |
158 | 157 |
|
... | ... | |
207 | 206 |
self._id2rpc.clear() |
208 | 207 |
|
209 | 208 |
|
209 |
class RaiseMode(object): |
|
210 |
|
|
211 |
NONE = 0 |
|
212 |
"Don't attempt to raise any type of `rpc-error` as :exc:`RPCError`." |
|
213 |
|
|
214 |
ERRORS = 1 |
|
215 |
"Raise only when the `error-type` indicates it is an honest-to-god error." |
|
216 |
|
|
217 |
ALL = 2 |
|
218 |
"Don't look at the `error-type`, always raise." |
|
219 |
|
|
220 |
|
|
210 | 221 |
class RPC(object): |
211 | 222 |
|
212 | 223 |
"""Base class for all operations, directly corresponding to *rpc* requests. Handles making the |
213 | 224 |
request, and taking delivery of the reply.""" |
214 |
|
|
225 |
|
|
215 | 226 |
DEPENDS = [] |
216 |
"""Subclasses can specify their dependencies on capabilities. List of URI's or abbreviated |
|
217 |
names, e.g. ':writable-running'. These are verified at the time of instantiation. If the |
|
218 |
capability is not available, a :exc:`MissingCapabilityError` is raised. |
|
227 |
"""Subclasses can specify their dependencies on capabilities. List of URI's or abbreviated names, e.g. ':writable-running'. These are verified at the time of instantiation. If the capability is not available, a :exc:`MissingCapabilityError` is raised. |
|
219 | 228 |
""" |
220 | 229 |
|
221 | 230 |
REPLY_CLS = RPCReply |
222 | 231 |
"Subclasses can specify a different reply class, but it should be a subclass of `RPCReply`." |
223 | 232 |
|
224 |
def __init__(self, session, async=False, timeout=None, raise_mode="none"):
|
|
233 |
def __init__(self, session, async=False, timeout=30, raise_mode=RaiseMode.NONE):
|
|
225 | 234 |
self._session = session |
226 | 235 |
try: |
227 | 236 |
for cap in self.DEPENDS: |
... | ... | |
245 | 254 |
return to_xml(ele) |
246 | 255 |
|
247 | 256 |
def _request(self, op): |
248 |
"""Implementations of :meth:`request` call this method to send the request and process the |
|
249 |
reply. |
|
257 |
"""Implementations of :meth:`request` call this method to send the request and process the reply. |
|
250 | 258 |
|
251 |
In synchronous mode, blocks until the reply is received and returns `RPCReply`. Depending on |
|
252 |
the :attr:`raise_mode` a *rpc-error* element in the reply may lead to an :exc:`RPCError` |
|
253 |
exception. |
|
259 |
In synchronous mode, blocks until the reply is received and returns :class:`RPCReply`. Depending on the :attr:`raise_mode` a `rpc-error` element in the reply may lead to an :exc:`RPCError` exception. |
|
254 | 260 |
|
255 |
In asynchronous mode, returns immediately, returning *self*. The :attr:`event` attribute |
|
256 |
will be set when the reply has been received (see :attr:`reply`) or an error occured (see |
|
257 |
:attr:`error`). |
|
261 |
In asynchronous mode, returns immediately, returning *self*. The :attr:`event` attribute will be set when the reply has been received (see :attr:`reply`) or an error occured (see :attr:`error`). |
|
258 | 262 |
|
259 |
:param op: operation to be requested |
|
263 |
:param op: operation to b\e requested
|
|
260 | 264 |
:type ops: `~xml.etree.ElementTree.Element` |
261 | 265 |
|
262 | 266 |
:rtype: `RPCReply` (sync) or `RPC` (async) |
... | ... | |
277 | 281 |
self._reply.parse() |
278 | 282 |
if self._reply.error is not None: |
279 | 283 |
# <rpc-error>'s [ RPCError ] |
280 |
if self._raise_mode == "all":
|
|
284 |
if self._raise_mode == RaiseMode.ALL:
|
|
281 | 285 |
raise self._reply.error |
282 |
elif (self._raise_mode == "errors" and |
|
283 |
self._reply.error.type == "error"): |
|
286 |
elif (self._raise_mode == RaiseMode.ERRORS and self._reply.error.type == "error"): |
|
284 | 287 |
raise self._reply.error |
285 | 288 |
return self._reply |
286 | 289 |
else: |
... | ... | |
311 | 314 |
|
312 | 315 |
@property |
313 | 316 |
def reply(self): |
314 |
"`RPCReply` element if reply has been received or :const:`None`"
|
|
317 |
":class:`RPCReply` element if reply has been received or `None`"
|
|
315 | 318 |
return self._reply |
316 | 319 |
|
317 | 320 |
@property |
318 | 321 |
def error(self): |
319 |
""":exc:`Exception` type if an error occured or :const:`None`.
|
|
322 |
""":exc:`Exception` type if an error occured or `None`. |
|
320 | 323 |
|
321 | 324 |
.. note:: |
322 | 325 |
This represents an error which prevented a reply from being received. An *rpc-error* |
... | ... | |
341 | 344 |
""" |
342 | 345 |
return self._event |
343 | 346 |
|
344 |
def set_async(self, async=True): |
|
347 |
def __set_async(self, async=True):
|
|
345 | 348 |
self._async = async |
346 | 349 |
if async and not session.can_pipeline: |
347 | 350 |
raise UserWarning('Asynchronous mode not supported for this device/session') |
348 | 351 |
|
349 |
def set_raise_mode(self, mode): |
|
352 |
def __set_raise_mode(self, mode):
|
|
350 | 353 |
assert(choice in ("all", "errors", "none")) |
351 | 354 |
self._raise_mode = mode |
352 | 355 |
|
353 |
def set_timeout(self, timeout): |
|
356 |
def __set_timeout(self, timeout):
|
|
354 | 357 |
self._timeout = timeout |
355 | 358 |
|
356 |
raise_mode = property(fget=lambda self: self._raise_mode, fset=set_raise_mode) |
|
357 |
"""Depending on this exception raising mode, an *rpc-error* in the reply may be raised as |
|
358 |
:exc:`RPCError` exceptions. Valid values: |
|
359 |
raise_mode = property(fget=lambda self: self._raise_mode, fset=__set_raise_mode) |
|
360 |
"""Depending on this exception raising mode, an `rpc-error` in the reply may be raised as an :exc:`RPCError` exception. Valid values are the constants defined in :class:`RaiseMode`. """ |
|
359 | 361 |
|
360 |
* ``"all"`` -- any kind of *rpc-error* (error or warning) |
|
361 |
* ``"errors"`` -- when the *error-type* element says it is an error |
|
362 |
* ``"none"`` -- neither |
|
363 |
""" |
|
364 |
|
|
365 |
is_async = property(fget=lambda self: self._async, fset=set_async) |
|
366 |
"""Specifies whether this RPC will be / was requested asynchronously. By default RPC's are |
|
367 |
synchronous. |
|
368 |
""" |
|
362 |
is_async = property(fget=lambda self: self._async, fset=__set_async) |
|
363 |
"""Specifies whether this RPC will be / was requested asynchronously. By default RPC's are synchronous. """ |
|
369 | 364 |
|
370 |
timeout = property(fget=lambda self: self._timeout, fset=set_timeout) |
|
371 |
"""Timeout in seconds for synchronous waiting defining how long the RPC request will block on a |
|
372 |
reply before raising :exc:`TimeoutExpiredError`. By default there is no timeout, represented by |
|
373 |
:const:`None`. |
|
365 |
timeout = property(fget=lambda self: self._timeout, fset=__set_timeout) |
|
366 |
"""Timeout in seconds for synchronous waiting defining how long the RPC request will block on a reply before raising :exc:`TimeoutExpiredError`. |
|
374 | 367 |
|
375 | 368 |
Irrelevant for asynchronous usage. |
376 | 369 |
""" |
b/ncclient/operations/session.py | ||
---|---|---|
12 | 12 |
# See the License for the specific language governing permissions and |
13 | 13 |
# limitations under the License. |
14 | 14 |
|
15 |
'Session-related NETCONF operations'
|
|
15 |
"Session-related NETCONF operations"
|
|
16 | 16 |
|
17 | 17 |
from ncclient.xml_ import * |
18 | 18 |
|
... | ... | |
20 | 20 |
|
21 | 21 |
class CloseSession(RPC): |
22 | 22 |
|
23 |
"*<close-session>* RPC. The connection to NETCONF server is also closed."
|
|
23 |
"`close-session` RPC. The connection to NETCONF server is also closed."
|
|
24 | 24 |
|
25 | 25 |
def request(self): |
26 |
"Request graceful termination of the NETCONF session, and also close the transport." |
|
26 | 27 |
try: |
27 | 28 |
return self._request(new_ele("close-session")) |
28 | 29 |
finally: |
... | ... | |
31 | 32 |
|
32 | 33 |
class KillSession(RPC): |
33 | 34 |
|
34 |
"*<kill-session>* RPC."
|
|
35 |
|
|
35 |
"`kill-session` RPC."
|
|
36 |
|
|
36 | 37 |
def request(self, session_id): |
37 |
""" |
|
38 |
:arg session_id: *session-id* of NETCONF session to kill |
|
39 |
:type session_id: `string`
|
|
38 |
"""Force the termination of a NETCONF session (not the current one!)
|
|
39 |
|
|
40 |
*session_id* is the session identifier of the NETCONF session to be terminated as a string
|
|
40 | 41 |
""" |
41 | 42 |
node = new_ele("kill-session") |
42 |
if not isinstance(session_id, basestring): # make sure |
|
43 |
session_id = str(session_id) |
|
44 | 43 |
sub_ele(node, "session-id").text = session_id |
45 | 44 |
return self._request(node) |
b/ncclient/transport/session.py | ||
---|---|---|
131 | 131 |
raise NotImplementedError |
132 | 132 |
|
133 | 133 |
def send(self, message): |
134 |
"""Send the supplied *message* to NETCONF server. |
|
135 |
|
|
136 |
:arg message: an XML document |
|
137 |
|
|
138 |
:type message: `string` |
|
139 |
""" |
|
134 |
"""Send the supplied *message* (xml string) to NETCONF server.""" |
|
140 | 135 |
if not self.connected: |
141 | 136 |
raise TransportError('Not connected to NETCONF server') |
142 | 137 |
logger.debug('queueing %s' % message) |
... | ... | |
161 | 156 |
|
162 | 157 |
@property |
163 | 158 |
def id(self): |
164 |
"""A `string` representing the `session-id`. If the session has not |
|
165 |
been initialized it will be :const:`None`""" |
|
159 |
"""A string representing the `session-id`. If the session has not been initialized it will be `None`""" |
|
166 | 160 |
return self._id |
167 | 161 |
|
168 | 162 |
|
... | ... | |
176 | 170 |
""" |
177 | 171 |
|
178 | 172 |
def callback(self, root, raw): |
179 |
"""Called when a new XML document is received. The `root` argument |
|
180 |
allows the callback to determine whether it wants to further process the |
|
181 |
document. |
|
173 |
"""Called when a new XML document is received. The *root* argument allows the callback to determine whether it wants to further process the document. |
|
182 | 174 |
|
183 |
:arg root: tuple of `(tag, attributes)` where `tag` is the qualified name of the root element and `attributes` is a dictionary of its attributes (also qualified names) |
|
184 |
:type root: `tuple` |
|
175 |
Here, *root* is a tuple of *(tag, attributes)* where *tag* is the qualified name of the root element and *attributes* is a dictionary of its attributes (also qualified names). |
|
185 | 176 |
|
186 |
:arg raw: XML document |
|
187 |
:type raw: `string` |
|
177 |
*raw* will contain the XML document as a string. |
|
188 | 178 |
""" |
189 | 179 |
raise NotImplementedError |
190 | 180 |
|
b/ncclient/transport/ssh.py | ||
---|---|---|
32 | 32 |
TICK = 0.1 |
33 | 33 |
|
34 | 34 |
def default_unknown_host_cb(host, fingerprint): |
35 |
"""An `unknown host callback` returns :const:`True` if it finds the key acceptable, and |
|
36 |
:const:`False` if not. |
|
35 |
"""An unknown host callback returns `True` if it finds the key acceptable, and `False` if not. |
|
37 | 36 |
|
38 |
This default callback always returns :const:`False`, which would lead to :meth:`connect` raising |
|
39 |
a :exc:`SSHUnknownHost` exception. |
|
37 |
This default callback always returns `False`, which would lead to :meth:`connect` raising a :exc:`SSHUnknownHost` exception. |
|
40 | 38 |
|
41 | 39 |
Supply another valid callback if you need to verify the host key programatically. |
42 | 40 |
|
43 |
:arg host: the hostname that needs to be verified |
|
44 |
:type host: `string` |
|
41 |
*host* is the hostname that needs to be verified |
|
45 | 42 |
|
46 |
:arg fingerprint: a hex string representing the host key fingerprint, colon-delimited e.g. *4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21* |
|
47 |
:type fingerprint: `string` |
|
43 |
*fingerprint* is a hex string representing the host key fingerprint, colon-delimited e.g. `"4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21"` |
|
48 | 44 |
""" |
49 | 45 |
return False |
50 | 46 |
|
... | ... | |
70 | 66 |
self._parsing_pos = 0 |
71 | 67 |
|
72 | 68 |
def _parse(self): |
73 |
"""Messages ae delimited by MSG_DELIM. The buffer could have grown by a |
|
74 |
maximum of BUF_SIZE bytes everytime this method is called. Retains state |
|
75 |
across method calls and if a byte has been read it will not be |
|
76 |
considered again. |
|
77 |
""" |
|
69 |
"Messages ae delimited by MSG_DELIM. The buffer could have grown by a maximum of BUF_SIZE bytes everytime this method is called. Retains state across method calls and if a byte has been read it will not be considered again." |
|
78 | 70 |
delim = MSG_DELIM |
79 | 71 |
n = len(delim) - 1 |
80 | 72 |
expect = self._parsing_state |
... | ... | |
115 | 107 |
self._parsing_pos = self._buffer.tell() |
116 | 108 |
|
117 | 109 |
def load_known_hosts(self, filename=None): |
118 |
"""Load host keys from a :file:`known_hosts`-style file. Can be called multiple times. |
|
110 |
"""Load host keys from an openssh :file:`known_hosts`-style file. Can be called multiple times.
|
|
119 | 111 |
|
120 |
If *filename* is not specified, looks in the default locations i.e. |
|
121 |
:file:`~/.ssh/known_hosts` and :file:`~/ssh/known_hosts` for Windows. |
|
112 |
If *filename* is not specified, looks in the default locations i.e. :file:`~/.ssh/known_hosts` and :file:`~/ssh/known_hosts` for Windows. |
|
122 | 113 |
""" |
123 | 114 |
if filename is None: |
124 | 115 |
filename = os.path.expanduser('~/.ssh/known_hosts') |
... | ... | |
139 | 130 |
self._transport.close() |
140 | 131 |
self._connected = False |
141 | 132 |
|
142 |
def connect(self, host, port=830, timeout=None, |
|
143 |
unknown_host_cb=default_unknown_host_cb, |
|
144 |
username=None, password=None, |
|
145 |
key_filename=None, allow_agent=True, look_for_keys=True): |
|
146 |
"""Connect via SSH and initialize the NETCONF session. First attempts the publickey |
|
147 |
authentication method and then password authentication. |
|
133 |
# REMEMBER to update transport.rst if sig. changes, since it is hardcoded there |
|
134 |
def connect(self, host, port=830, timeout=None, unknown_host_cb=default_unknown_host_cb, |
|
135 |
username=None, password=None, key_filename=None, allow_agent=True, look_for_keys=True): |
|
136 |
"""Connect via SSH and initialize the NETCONF session. First attempts the publickey authentication method and then password authentication. |
|
148 | 137 |
|
149 |
To disable attemting publickey authentication altogether, call with *allow_agent* and |
|
150 |
*look_for_keys* as :const:`False`. |
|
138 |
To disable attempting publickey authentication altogether, call with *allow_agent* and *look_for_keys* as `False`. |
|
151 | 139 |
|
152 | 140 |
:arg host: the hostname or IP address to connect to |
153 |
:type host: `string`
|
|
141 |
:type host: string
|
|
154 | 142 |
|
155 | 143 |
:arg port: by default 830, but some devices use the default SSH port of 22 so this may need to be specified |
156 |
:type port: `int`
|
|
144 |
:type port: int
|
|
157 | 145 |
|
158 | 146 |
:arg timeout: an optional timeout for socket connect |
159 |
:type timeout: `int`
|
|
147 |
:type timeout: int
|
|
160 | 148 |
|
161 | 149 |
:arg unknown_host_cb: called when the server host key is not recognized |
162 |
:type unknown_host_cb: see :meth:`signature <ssh.default_unknown_host_cb>`
|
|
150 |
:type unknown_host_cb: see the signature of :func:`default_unknown_host_cb`
|
|
163 | 151 |
|
164 | 152 |
:arg username: the username to use for SSH authentication |
165 |
:type username: `string`
|
|
153 |
:type username: string
|
|
166 | 154 |
|
167 | 155 |
:arg password: the password used if using password authentication, or the passphrase to use for unlocking keys that require it |
168 |
:type password: `string`
|
|
156 |
:type password: string
|
|
169 | 157 |
|
170 | 158 |
:arg key_filename: a filename where a the private key to be used can be found |
171 |
:type key_filename: `string`
|
|
159 |
:type key_filename: string
|
|
172 | 160 |
|
173 | 161 |
:arg allow_agent: enables querying SSH agent (if found) for keys |
174 |
:type allow_agent: `bool`
|
|
162 |
:type allow_agent: bool
|
|
175 | 163 |
|
176 | 164 |
:arg look_for_keys: enables looking in the usual locations for ssh keys (e.g. :file:`~/.ssh/id_*`) |
177 |
:type look_for_keys: `bool`
|
|
165 |
:type look_for_keys: bool
|
|
178 | 166 |
""" |
179 |
|
|
180 | 167 |
if username is None: |
181 | 168 |
username = getpass.getuser() |
182 | 169 |
|
... | ... | |
306 | 293 |
q = self._q |
307 | 294 |
try: |
308 | 295 |
while True: |
309 |
# select on a paramiko ssh channel object does not ever return |
|
310 |
# it in the writable list, so it channel's don't exactly emulate |
|
311 |
# the socket api |
|
296 |
# select on a paramiko ssh channel object does not ever return it in the writable list, so channels don't exactly emulate the socket api |
|
312 | 297 |
r, w, e = select([chan], [], [], TICK) |
313 |
# will wakeup evey TICK seconds to check if something |
|
314 |
# to send, more if something to read (due to select returning |
|
315 |
# chan in readable list) |
|
298 |
# will wakeup evey TICK seconds to check if something to send, more if something to read (due to select returning chan in readable list) |
|
316 | 299 |
if r: |
317 | 300 |
data = chan.recv(BUF_SIZE) |
318 | 301 |
if data: |
... | ... | |
335 | 318 |
|
336 | 319 |
@property |
337 | 320 |
def transport(self): |
338 |
"""Underlying `paramiko.Transport |
|
339 |
<http://www.lag.net/paramiko/docs/paramiko.Transport-class.html>`_ |
|
340 |
object. This makes it possible to call methods like set_keepalive on it. |
|
341 |
""" |
|
321 |
"Underlying `paramiko.Transport <http://www.lag.net/paramiko/docs/paramiko.Transport-class.html>`_ object. This makes it possible to call methods like set_keepalive on it." |
|
342 | 322 |
return self._transport |
b/ncclient/xml_.py | ||
---|---|---|
41 | 41 |
from xml.etree import ElementTree |
42 | 42 |
# cElementTree uses ElementTree's _namespace_map, so that's ok |
43 | 43 |
ElementTree._namespace_map[uri] = prefix |
44 |
register_namespace.func_doc = "ElementTree's namespace map determines the prefixes for namespace URI's when serializing to XML. This method allows modifying this map to specify a prefix for a namespace URI." |
|
44 | 45 |
|
45 | 46 |
for (ns, pre) in { |
46 | 47 |
BASE_NS_1_0: 'nc', |
... | ... | |
51 | 52 |
}.items(): register_namespace(pre, ns) |
52 | 53 |
|
53 | 54 |
qualify = lambda tag, ns=BASE_NS_1_0: tag if ns is None else "{%s}%s" % (ns, tag) |
54 |
"""Qualify a tag name with a namespace, in :mod:`~xml.etree.ElementTree` fashion i.e. *{namespace}tagname*. |
|
55 |
|
|
56 |
:arg tag: name of the tag |
|
57 |
:type arg: `string` |
|
58 |
|
|
59 |
:arg ns: namespace to qualify with |
|
60 |
:type ns: `string` |
|
61 |
""" |
|
55 |
"""Qualify a *tag* name with a *namespace*, in :mod:`~xml.etree.ElementTree` fashion i.e. *{namespace}tagname*.""" |
|
62 | 56 |
|
63 | 57 |
def to_xml(ele, encoding="UTF-8"): |
64 |
"""Convert an `~xml.etree.ElementTree.Element` to XML. |
|
65 |
|
|
66 |
:arg ele: the `~xml.etree.ElementTree.Element` |
|
67 |
:arg encoding: character encoding |
|
68 |
:rtype: `string` |
|
69 |
""" |
|
58 |
"Convert and return the XML for an *ele* (:class:`~xml.etree.ElementTree.Element`) with specified *encoding*." |
|
70 | 59 |
xml = ET.tostring(ele, encoding) |
71 | 60 |
return xml if xml.startswith('<?xml') else '<?xml version="1.0" encoding="%s"?>%s' % (encoding, xml) |
72 | 61 |
|
73 | 62 |
def to_ele(x): |
74 |
"""Convert XML to `~xml.etree.ElementTree.Element`. If passed an `~xml.etree.ElementTree.Element` simply returns that. |
|
75 |
|
|
76 |
:arg x: the XML document or element |
|
77 |
:type x: `string` or `~xml.etree.ElementTree.Element` |
|
78 |
:rtype: `~xml.etree.ElementTree.Element` |
|
79 |
""" |
|
63 |
"Convert and return the :class:`~xml.etree.ElementTree.Element` for the XML document *x*. If *x* is already an :class:`~xml.etree.ElementTree.Element` simply returns that." |
|
80 | 64 |
return x if ET.iselement(x) else ET.fromstring(x) |
81 | 65 |
|
82 | 66 |
def parse_root(raw): |
83 |
"""Efficiently parses the root element of an XML document. |
|
84 |
|
|
85 |
:arg raw: XML document |
|
86 |
:returns: a tuple of ``(tag, attrib)``, where *tag* is the (qualified) name of the element and *attrib* is a dictionary of its attributes. |
|
87 |
:rtype: `tuple` |
|
88 |
""" |
|
67 |
"Efficiently parses the root element of a *raw* XML document, returning a tuple of its qualified name and attribute dictionary." |
|
89 | 68 |
fp = StringIO(raw) |
90 | 69 |
for event, element in ET.iterparse(fp, events=('start',)): |
91 | 70 |
return (element.tag, element.attrib) |
... | ... | |
93 | 72 |
def validated_element(x, tags=None, attrs=None): |
94 | 73 |
"""Checks if the root element of an XML document or Element meets the supplied criteria. |
95 | 74 |
|
96 |
:arg tags: allowable tag name or sequence of allowable alternatives |
|
97 |
:type tags: `string` or sequence of strings |
|
98 |
|
|
99 |
:arg attrs: list of required attributes, each of which may be a sequence of several allowable alternatives |
|
100 |
:type attrs: sequence of strings or sequence of sequences of strings |
|
101 |
|
Also available in: Unified diff