Revision 4bc8021f
b/docs/source/_templates/layout.html | ||
---|---|---|
1 | 1 |
{% extends "!layout.html" %} |
2 | 2 |
{% block sidebarlogo %} |
3 | 3 |
<p align="center"> |
4 |
<a href="http://code.google.com/p/ncclient">
|
|
4 |
<a href="http://schmizz.net/ncclient">
|
|
5 | 5 |
<img src="_static/logo.png" width="125" height="125"/> |
6 | 6 |
</a> |
7 | 7 |
</p> |
b/docs/source/api.rst | ||
---|---|---|
1 | 1 |
Complete API documentation |
2 | 2 |
========================== |
3 | 3 |
|
4 |
**TODO** |
|
5 |
|
|
6 | 4 |
.. toctree:: |
7 | 5 |
|
8 | 6 |
capabilities |
b/docs/source/capabilities.rst | ||
---|---|---|
1 |
:mod:`~ncclient.capabilities` -- NETCONF Capabilities |
|
2 |
===================================================== |
|
3 |
|
|
4 |
.. module:: ncclient.capabilities |
|
5 |
|
|
6 |
.. autofunction:: schemes |
|
7 |
|
|
8 |
.. autoclass:: Capabilities |
|
9 |
:members: |
b/docs/source/manager.rst | ||
---|---|---|
1 | 1 |
:mod:`~ncclient.manager` -- High-level API |
2 | 2 |
========================================== |
3 | 3 |
|
4 |
.. module:: ncclient.manager |
|
4 |
.. automodule:: ncclient.manager
|
|
5 | 5 |
:synopsis: High-level API |
6 | 6 |
|
7 |
.. data:: CAPABILITIES |
|
7 |
Module data |
|
8 |
----------- |
|
8 | 9 |
|
9 |
List of URI strings representing the client's capabilities. This is used during the initial
|
|
10 |
capability exchange. Modify this if you need to announce some capability not already included.
|
|
10 |
These attributes control what capabilties are exchanged with the NETCONF server and what operations
|
|
11 |
are available through the `Manager` API.
|
|
11 | 12 |
|
12 |
.. data:: OPERATIONS |
|
13 |
|
|
14 |
Dictionary of method names and corresponding `~ncclient.operations.RPC` classes. `Manager` uses |
|
15 |
this to lookup operations, e.g. "get_config" is mapped to `ncclient.operations.GetConfig`. It |
|
16 |
is thus possible to add additional operations to the `Manager` API. |
|
13 |
.. autodata:: CAPABILITIES |
|
14 |
|
|
15 |
.. autodata:: OPERATIONS |
|
17 | 16 |
|
18 | 17 |
Factory functions |
19 | 18 |
----------------- |
20 | 19 |
|
21 | 20 |
A `Manager` instance is created using a factory function. |
22 | 21 |
|
23 |
.. function:: 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]) |
|
24 |
|
|
25 |
Initializes a NETCONF session over SSH, and creates a connected `Manager` instance. *host* must |
|
26 |
be specified, all the other arguments are optional and depend on the kind of host key |
|
27 |
verification and user authentication you want to complete. |
|
28 |
|
|
29 |
For the purpose of host key verification, on -NIX systems a user's :file:`~/.ssh/known_hosts` |
|
30 |
file is automatically considered. The *unknown_host_cb* argument specifies a callback that will |
|
31 |
be invoked when the server's host key cannot be verified. See |
|
32 |
:func:`~ncclient.transport.ssh.default_known_host_cb` for function signature. |
|
33 |
|
|
34 |
First, ``publickey`` authentication is attempted. If a specific *key_filename* is specified, it |
|
35 |
will be loaded and authentication attempted using it. If *allow_agent* is :const:`True` and an |
|
36 |
SSH agent is running, the keys provided by the agent will be tried. If *look_for_keys* is |
|
37 |
:const:`True`, keys in the :file:`~/.ssh/id_rsa` and :file:`~.ssh/id_dsa` will be tried. In case |
|
38 |
an encrypted key file is encountered, the *password* argument will be used as a decryption |
|
39 |
passphrase. |
|
40 |
|
|
41 |
If ``publickey`` authentication fails and the *password* argument has been supplied, |
|
42 |
``password`` / ``keyboard-interactive`` SSH authentication will be attempted. |
|
43 |
|
|
44 |
:param host: hostname or address on which to connect |
|
45 |
:type host: `string` |
|
46 |
|
|
47 |
:param port: port on which to connect |
|
48 |
:type port: `int` |
|
49 |
|
|
50 |
:param timeout: timeout for socket connect |
|
51 |
:type timeout: `int` |
|
52 |
|
|
53 |
:param unknown_host_cb: optional; callback that is invoked when host key verification fails |
|
54 |
:type unknown_host_cb: `function` |
|
55 |
|
|
56 |
:param username: username to authenticate with, if not specified the username of the logged-in |
|
57 |
user is used |
|
58 |
:type username: `string` |
|
59 |
|
|
60 |
:param password: password for ``password`` authentication or passphrase for decrypting |
|
61 |
private key files |
|
62 |
:type password: `string` |
|
63 |
|
|
64 |
:param key_filename: location of a private key file on the file system |
|
65 |
:type key_filename: `string` |
|
66 |
|
|
67 |
:param allow_agent: whether to try connecting to SSH agent for keys |
|
68 |
:type allow_agent: `bool` |
|
69 |
|
|
70 |
:param look_for_keys: whether to look in usual locations for keys |
|
71 |
:type look_for_keys: `bool` |
|
72 |
|
|
73 |
:raises: :exc:`~ncclient.transport.SSHUnknownHostError` |
|
74 |
:raises: :exc:`~ncclient.transport.AuthenticationError` |
|
75 |
|
|
76 |
:rtype: `Manager` |
|
77 |
|
|
78 |
.. function:: connect() |
|
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]) |
|
79 | 23 |
|
80 |
Same as :func:`connect_ssh`, since SSH is the default (and currently, the |
|
81 |
only) transport. |
|
24 |
.. autodata:: connect |
|
82 | 25 |
|
83 | 26 |
Manager |
84 | 27 |
------- |
... | ... | |
263 | 206 |
Valid values: |
264 | 207 |
|
265 | 208 |
* ``"all"`` -- any kind of *rpc-error* (error or warning) |
266 |
* ``"errors"`` -- where the *error-type* attribute says it is an error
|
|
209 |
* ``"errors"`` -- where the *error-type* element says it is an error
|
|
267 | 210 |
* ``"none"`` -- neither |
268 | 211 |
|
269 | 212 |
.. attribute:: client_capabilities |
b/docs/source/operations.rst | ||
---|---|---|
1 |
:mod:`~ncclient.operations` -- Everything RPC |
|
2 |
============================================= |
|
3 |
|
|
4 |
Base classes |
|
5 |
------------ |
|
6 |
|
|
7 |
.. module:: ncclient.operations |
|
8 |
:synopsis: Everything RPC |
|
9 |
|
|
10 |
.. autoclass:: RPC(session[, async=False, timeout=None, raise_mode="none"]) |
|
11 |
:members: DEPENDS, REPLY_CLS, _assert, _request, request, event, error, reply, raise_mode, is_async, timeout |
|
12 |
|
|
13 |
.. autoclass:: RPCReply |
|
14 |
:members: xml, ok, error, errors |
|
15 |
|
|
16 |
.. autoexception:: RPCError |
|
17 |
:show-inheritance: |
|
18 |
:members: type, severity, tag, path, message, info |
|
19 |
|
|
20 |
Operations |
|
21 |
---------- |
|
22 |
|
|
23 |
*TODO* The operation classes are currently undocumented. See documentation of |
|
24 |
`~ncclient.manager.Manager` for methods that utilize the operation classes. The parameters accepted |
|
25 |
by :meth:`~RPC.request` for these classes are the same. |
|
26 |
|
|
27 |
Replies with data |
|
28 |
----------------- |
|
29 |
|
|
30 |
.. autoclass:: GetReply |
|
31 |
:show-inheritance: |
|
32 |
:members: data, data_ele, data_xml |
|
33 |
|
|
34 |
Exceptions |
|
35 |
---------- |
|
36 |
|
|
37 |
.. autoexception:: OperationError |
|
38 |
:show-inheritance: |
|
39 |
|
|
40 |
.. autoexception:: MissingCapabilityError |
|
41 |
:show-inheritance: |
|
42 |
|
|
43 |
.. autoexception:: TimeoutExpiredError |
|
44 |
:show-inheritance: |
b/docs/source/transport.rst | ||
---|---|---|
1 |
:mod:`~ncclient.transport` -- Transport / Session layer |
|
2 |
======================================================= |
|
3 |
|
|
4 |
.. module:: ncclient.transport |
|
5 |
:synopsis: Transport / Session layer |
|
6 |
|
|
7 |
Base types |
|
8 |
----------- |
|
9 |
|
|
10 |
.. autoclass:: Session |
|
11 |
:members: add_listener, remove_listener, get_listener_instance, client_capabilities, server_capabilities, connected, id |
|
12 |
|
|
13 |
.. autoclass:: SessionListener |
|
14 |
:members: callback, errback |
|
15 |
|
|
16 |
SSH session implementation |
|
17 |
-------------------------- |
|
18 |
|
|
19 |
.. automethod:: ssh.default_unknown_host_cb |
|
20 |
|
|
21 |
.. autoclass:: SSHSession |
|
22 |
:show-inheritance: |
|
23 |
:members: load_known_hosts, close, transport |
|
24 |
|
|
25 |
.. automethod:: connect(host[, port=830, timeout=None, username=None, password=None, key_filename=None, allow_agent=True, look_for_keys=True]) |
|
26 |
|
|
27 |
Errors |
|
28 |
------ |
|
29 |
|
|
30 |
.. autoexception:: TransportError |
|
31 |
:show-inheritance: |
|
32 |
|
|
33 |
.. autoexception:: SessionCloseError |
|
34 |
:show-inheritance: |
|
35 |
|
|
36 |
.. autoexception:: SSHError |
|
37 |
:show-inheritance: |
|
38 |
|
|
39 |
.. autoexception:: AuthenticationError |
|
40 |
:show-inheritance: |
|
41 |
|
|
42 |
.. autoexception:: SSHUnknownHostError |
|
43 |
:show-inheritance: |
|
44 |
|
b/docs/source/xml_.rst | ||
---|---|---|
1 |
:mod:`~ncclient.xml_` -- XML Handling |
|
2 |
===================================== |
|
3 |
|
|
4 |
*TODO* |
b/ncclient/capabilities.py | ||
---|---|---|
29 | 29 |
return [] |
30 | 30 |
|
31 | 31 |
def schemes(url_uri): |
32 |
"""Given a URI that has a *scheme* query string (i.e. *:url* capability |
|
33 |
URI), will return a list of supported schemes.
|
|
32 |
"""Given a URI that has a *scheme* query string (i.e. *:url* capability URI), will return a list
|
|
33 |
of supported schemes. |
|
34 | 34 |
""" |
35 | 35 |
return url_uri.partition("?scheme=")[2].split(",") |
36 | 36 |
|
37 | 37 |
class Capabilities: |
38 | 38 |
|
39 |
"""Represents the set of capabilities available to a NETCONF client or |
|
40 |
server.
|
|
39 |
"""Represents the set of capabilities available to a NETCONF client or server. It is initialized
|
|
40 |
with a list of capability URI's. These can be iterated over.
|
|
41 | 41 |
|
42 |
Presence of a capability can be checked with the *in* operation. In addition |
|
43 |
to the URI, for capabilities of the form |
|
44 |
*urn:ietf:params:netconf:capability:$name:$version* their shorthand can be |
|
45 |
used as a key. For example, for |
|
46 |
*urn:ietf:params:netconf:capability:candidate:1.0* the shorthand would be |
|
47 |
*:candidate*. If version is significant, use *:candidate:1.0* as key. |
|
42 |
Presence of a capability can be checked with the *in* operation. In addition to the URI, for |
|
43 |
capabilities of the form *urn:ietf:params:netconf:capability:$name:$version* their shorthand can |
|
44 |
be used as a key. For example, for *urn:ietf:params:netconf:capability:candidate:1.0* the |
|
45 |
shorthand would be *:candidate*. If version is significant, use *:candidate:1.0* as key. |
|
48 | 46 |
""" |
49 | 47 |
|
50 | 48 |
def __init__(self, capabilities): |
51 |
"Initializes with a list of capability URI's" |
|
52 | 49 |
self._dict = {} |
53 | 50 |
for uri in capabilities: |
54 | 51 |
self._dict[uri] = _abbreviate(uri) |
... | ... | |
74 | 71 |
return self._dict.keys() |
75 | 72 |
|
76 | 73 |
def add(self, uri): |
77 |
"Add a capability" |
|
74 |
"Add a capability."
|
|
78 | 75 |
self._dict[uri] = _abbreviate(uri) |
79 | 76 |
|
80 | 77 |
def remove(self, uri): |
81 |
"Remove a capability" |
|
78 |
"Remove a capability."
|
|
82 | 79 |
if key in self._dict: |
83 | 80 |
del self._dict[key] |
84 | 81 |
|
85 |
def get_uri(self, shorthand): |
|
86 |
for uri, abbrs in self._dict.items(): |
|
87 |
if shorthand in abbrs: |
|
88 |
return uri |
|
82 |
#def get_uri(self, shorthand): |
|
83 |
# "Returns the URI that is inferred for a given shorthand." |
|
84 |
# for uri, abbrs in self._dict.items(): |
|
85 |
# if shorthand in abbrs: |
|
86 |
# return uri |
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 |
"Thin layer of abstraction around NCClient"
|
|
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 |
... | ... | |
22 | 22 |
logger = logging.getLogger('ncclient.manager') |
23 | 23 |
|
24 | 24 |
|
25 |
#: :class:`Capabilities` object representing the capabilities currently supported by NCClient |
|
26 | 25 |
CAPABILITIES = capabilities.Capabilities([ |
27 | 26 |
"urn:ietf:params:netconf:base:1.0", |
28 | 27 |
"urn:ietf:params:netconf:capability:writable-running:1.0", |
... | ... | |
37 | 36 |
"urn:ietf:params:netconf:capability:interleave:1.0" |
38 | 37 |
#'urn:ietf:params:netconf:capability:notification:1.0', # TODO |
39 | 38 |
]) |
40 |
|
|
39 |
"""`~ncclient.capabilities.Capabilities` object representing the client's capabilities. This is used |
|
40 |
during the initial capability exchange. Modify this if you need to announce some capability not |
|
41 |
already included. |
|
42 |
""" |
|
41 | 43 |
|
42 | 44 |
OPERATIONS = { |
43 | 45 |
"get": operations.Get, |
... | ... | |
55 | 57 |
"poweroff_machine": operations.PoweroffMachine, |
56 | 58 |
"reboot_machine": operations.RebootMachine |
57 | 59 |
} |
58 |
|
|
60 |
"""Dictionary of method names and corresponding `~ncclient.operations.RPC` subclasses. `Manager` |
|
61 |
uses this to lookup operations, e.g. "get_config" is mapped to `~ncclient.operations.GetConfig`. It |
|
62 |
is thus possible to add additional operations to the `Manager` API. |
|
63 |
""" |
|
59 | 64 |
|
60 | 65 |
def connect_ssh(*args, **kwds): |
66 |
"""Initializes a NETCONF session over SSH, and creates a connected `Manager` instance. *host* |
|
67 |
must be specified, all the other arguments are optional and depend on the kind of host key |
|
68 |
verification and user authentication you want to complete. |
|
69 |
|
|
70 |
For the purpose of host key verification, on -NIX systems a user's :file:`~/.ssh/known_hosts` |
|
71 |
file is automatically considered. The *unknown_host_cb* argument specifies a callback that will |
|
72 |
be invoked when the server's host key cannot be verified. See |
|
73 |
:func:`~ncclient.transport.ssh.default_unknown_host_cb` for function signature. |
|
74 |
|
|
75 |
First, ``publickey`` authentication is attempted. If a specific *key_filename* is specified, it |
|
76 |
will be loaded and authentication attempted using it. If *allow_agent* is :const:`True` and an |
|
77 |
SSH agent is running, the keys provided by the agent will be tried. If *look_for_keys* is |
|
78 |
:const:`True`, keys in the :file:`~/.ssh/id_rsa` and :file:`~.ssh/id_dsa` will be tried. In case |
|
79 |
an encrypted key file is encountered, the *password* argument will be used as a decryption |
|
80 |
passphrase. |
|
81 |
|
|
82 |
If ``publickey`` authentication fails and the *password* argument has been supplied, |
|
83 |
``password`` / ``keyboard-interactive`` SSH authentication will be attempted. |
|
84 |
|
|
85 |
:param host: hostname or address on which to connect |
|
86 |
:type host: `string` |
|
87 |
|
|
88 |
:param port: port on which to connect |
|
89 |
:type port: `int` |
|
90 |
|
|
91 |
:param timeout: timeout for socket connect |
|
92 |
:type timeout: `int` |
|
93 |
|
|
94 |
:param unknown_host_cb: optional; callback that is invoked when host key verification fails |
|
95 |
:type unknown_host_cb: `function` |
|
96 |
|
|
97 |
:param username: username to authenticate with, if not specified the username of the logged-in user is used |
|
98 |
:type username: `string` |
|
99 |
|
|
100 |
:param password: password for ``password`` authentication or passphrase for decrypting private key files |
|
101 |
:type password: `string` |
|
102 |
|
|
103 |
:param key_filename: location of a private key file on the file system |
|
104 |
:type key_filename: `string` |
|
105 |
|
|
106 |
:param allow_agent: whether to try connecting to SSH agent for keys |
|
107 |
:type allow_agent: `bool` |
|
108 |
|
|
109 |
:param look_for_keys: whether to look in usual locations for keys |
|
110 |
:type look_for_keys: `bool` |
|
111 |
|
|
112 |
:raises: :exc:`~ncclient.transport.SSHUnknownHostError` |
|
113 |
:raises: :exc:`~ncclient.transport.AuthenticationError` |
|
114 |
|
|
115 |
:rtype: `Manager` |
|
116 |
""" |
|
61 | 117 |
session = transport.SSHSession(CAPABILITIES) |
62 | 118 |
session.load_known_hosts() |
63 | 119 |
session.connect(*args, **kwds) |
64 | 120 |
return Manager(session) |
65 | 121 |
|
66 |
#: Same as :meth:`connect_ssh` |
|
67 | 122 |
connect = connect_ssh |
68 |
|
|
123 |
"Same as :func:`connect_ssh`, since SSH is the default (and currently, the only) transport." |
|
69 | 124 |
|
70 | 125 |
class Manager(object): |
71 | 126 |
|
b/ncclient/operations/retrieve.py | ||
---|---|---|
20 | 20 |
|
21 | 21 |
class GetReply(RPCReply): |
22 | 22 |
|
23 |
"""Adds attributes for the *<data>* element to :class:`RPCReply`, which
|
|
24 |
pertains to the :class:`Get` and :class:`GetConfig` operations."""
|
|
23 |
"""Adds attributes for the *data* element to `RPCReply`. This pertains to the `Get` and
|
|
24 |
`GetConfig` operations.""" |
|
25 | 25 |
|
26 | 26 |
def _parsing_hook(self, root): |
27 | 27 |
self._data = None |
28 | 28 |
if not self._errors: |
29 | 29 |
self._data = root.find(qualify("data")) |
30 |
|
|
30 |
|
|
31 | 31 |
@property |
32 | 32 |
def data_ele(self): |
33 |
"*<data>* element as an :class:`~xml.etree.ElementTree.Element`"
|
|
33 |
"*data* element as an `~xml.etree.ElementTree.Element`"
|
|
34 | 34 |
if not self._parsed: |
35 | 35 |
self.parse() |
36 | 36 |
return self._data |
37 | 37 |
|
38 | 38 |
@property |
39 | 39 |
def data_xml(self): |
40 |
"*<data>* element as an XML string"
|
|
40 |
"*data* element as an XML string"
|
|
41 | 41 |
if not self._parsed: |
42 | 42 |
self.parse() |
43 | 43 |
return to_xml(self._data) |
44 | 44 |
|
45 |
#: Same as :attr:`data_ele` |
|
46 | 45 |
data = data_ele |
47 |
|
|
48 |
#def __repr__(self): |
|
49 |
# return self.data_xml |
|
46 |
"Same as :attr:`data_ele`" |
|
50 | 47 |
|
51 | 48 |
|
52 | 49 |
class Get(RPC): |
53 | 50 |
|
54 |
"The *<get>* RPC"
|
|
51 |
"The *get* RPC."
|
|
55 | 52 |
|
56 | 53 |
REPLY_CLS = GetReply |
57 | 54 |
|
... | ... | |
64 | 61 |
|
65 | 62 |
class GetConfig(RPC): |
66 | 63 |
|
67 |
"The *<get-config>* RPC"
|
|
64 |
"The *get-config* RPC."
|
|
68 | 65 |
|
69 | 66 |
REPLY_CLS = GetReply |
70 | 67 |
|
b/ncclient/operations/rpc.py | ||
---|---|---|
24 | 24 |
logger = logging.getLogger("ncclient.operations.rpc") |
25 | 25 |
|
26 | 26 |
|
27 |
class RPCReply: |
|
28 |
|
|
29 |
"""Represents an *<rpc-reply>*. Only concerns itself with whether the |
|
30 |
operation was successful. |
|
31 |
|
|
32 |
.. note:: |
|
33 |
If the reply has not yet been parsed there is an implicit, one-time |
|
34 |
parsing overhead to accessing the attributes defined by this class and |
|
35 |
any subclasses. |
|
36 |
""" |
|
37 |
|
|
38 |
def __init__(self, raw): |
|
39 |
self._raw = raw |
|
40 |
self._parsed = False |
|
41 |
self._root = None |
|
42 |
self._errors = [] |
|
43 |
|
|
44 |
def __repr__(self): |
|
45 |
return self._raw |
|
46 |
|
|
47 |
def _parsing_hook(self, root): |
|
48 |
"""Subclass can implement. |
|
49 |
|
|
50 |
:type root: :class:`~xml.etree.ElementTree.Element` |
|
51 |
""" |
|
52 |
pass |
|
53 |
|
|
54 |
def parse(self): |
|
55 |
"""Parse the *<rpc-reply>*""" |
|
56 |
if self._parsed: |
|
57 |
return |
|
58 |
root = self._root = to_ele(self._raw) # The <rpc-reply> element |
|
59 |
# Per RFC 4741 an <ok/> tag is sent when there are no errors or warnings |
|
60 |
ok = root.find(qualify("ok")) |
|
61 |
if ok is None: |
|
62 |
# Create RPCError objects from <rpc-error> elements |
|
63 |
error = root.find(qualify("rpc-error")) |
|
64 |
if error is not None: |
|
65 |
for err in root.getiterator(error.tag): |
|
66 |
# Process a particular <rpc-error> |
|
67 |
self._errors.append(RPCError(err)) |
|
68 |
self._parsing_hook(root) |
|
69 |
self._parsed = True |
|
70 |
|
|
71 |
@property |
|
72 |
def xml(self): |
|
73 |
"*<rpc-reply>* as returned" |
|
74 |
return self._raw |
|
75 |
|
|
76 |
@property |
|
77 |
def ok(self): |
|
78 |
"Boolean value indicating if there were no errors." |
|
79 |
if not self._parsed: |
|
80 |
self.parse() |
|
81 |
return not self._errors # empty list => false |
|
82 |
|
|
83 |
@property |
|
84 |
def error(self): |
|
85 |
"""Short for :attr:`errors` [0]; :const:`None` if there were no errors. |
|
86 |
""" |
|
87 |
if not self._parsed: |
|
88 |
self.parse() |
|
89 |
if self._errors: |
|
90 |
return self._errors[0] |
|
91 |
else: |
|
92 |
return None |
|
93 |
|
|
94 |
@property |
|
95 |
def errors(self): |
|
96 |
"""`list` of :class:`RPCError` objects. Will be empty if there were no |
|
97 |
*<rpc-error>* elements in reply. |
|
98 |
""" |
|
99 |
if not self._parsed: |
|
100 |
self.parse() |
|
101 |
return self._errors |
|
102 |
|
|
103 |
|
|
104 | 27 |
class RPCError(OperationError): |
105 | 28 |
|
106 |
"""Represents an *<rpc-error>*. It is a type of :exc:`OperationError`
|
|
107 |
and can be raised like any other exception."""
|
|
29 |
"""Represents an *rpc-error*. It is a type of :exc:`OperationError` and can be raised like any
|
|
30 |
other exception.""" |
|
108 | 31 |
|
109 | 32 |
tag_to_attr = { |
110 | 33 |
qualify("error-type"): "_type", |
... | ... | |
115 | 38 |
qualify("error-message"): "_message" |
116 | 39 |
} |
117 | 40 |
|
118 |
def __init__(self, err): |
|
41 |
def __init__(self, raw): |
|
42 |
self._raw = raw |
|
119 | 43 |
for attr in RPCError.tag_to_attr.values(): |
120 | 44 |
setattr(self, attr, None) |
121 |
for subele in err:
|
|
45 |
for subele in raw:
|
|
122 | 46 |
attr = RPCError.tag_to_attr.get(subele.tag, None) |
123 | 47 |
if attr is not None: |
124 | 48 |
setattr(self, attr, subele.text if attr != "_info" else to_xml(subele) ) |
... | ... | |
130 | 54 |
def to_dict(self): |
131 | 55 |
return dict([ (attr[1:], getattr(self, attr)) for attr in RPCError.tag_to_attr.values() ]) |
132 | 56 |
|
57 |
"*rpc-error* element as returned." |
|
58 |
@property |
|
59 |
def xml(self): |
|
60 |
return self._raw |
|
61 |
|
|
133 | 62 |
@property |
134 | 63 |
def type(self): |
135 |
"`string` representing text of *error-type* element" |
|
64 |
"`string` representing text of *error-type* element."
|
|
136 | 65 |
return self._type |
137 | 66 |
|
138 | 67 |
@property |
139 | 68 |
def tag(self): |
140 |
"`string` representing text of *error-tag* element" |
|
69 |
"`string` representing text of *error-tag* element."
|
|
141 | 70 |
return self._tag |
142 | 71 |
|
143 | 72 |
@property |
144 | 73 |
def severity(self): |
145 |
"`string` representing text of *error-severity* element" |
|
74 |
"`string` representing text of *error-severity* element."
|
|
146 | 75 |
return self._severity |
147 | 76 |
|
148 | 77 |
@property |
149 | 78 |
def path(self): |
150 |
"`string` or :const:`None`; representing text of *error-path* element" |
|
79 |
"`string` or :const:`None`; representing text of *error-path* element."
|
|
151 | 80 |
return self._path |
152 | 81 |
|
153 | 82 |
@property |
154 | 83 |
def message(self): |
155 |
"`string` or :const:`None`; representing text of *error-message* element" |
|
84 |
"`string` or :const:`None`; representing text of *error-message* element."
|
|
156 | 85 |
return self._message |
157 | 86 |
|
158 | 87 |
@property |
159 | 88 |
def info(self): |
160 |
"`string` (XML) or :const:`None`, representing *error-info* element"
|
|
89 |
"`string` (XML) or :const:`None`; representing *error-info* element."
|
|
161 | 90 |
return self._info |
162 | 91 |
|
163 | 92 |
|
93 |
class RPCReply: |
|
94 |
|
|
95 |
"""Represents an *rpc-reply*. Only concerns itself with whether the operation was successful. |
|
96 |
|
|
97 |
.. note:: |
|
98 |
If the reply has not yet been parsed there is an implicit, one-time parsing overhead to |
|
99 |
accessing the attributes defined by this class and any subclasses. |
|
100 |
""" |
|
101 |
|
|
102 |
ERROR_CLS = RPCError |
|
103 |
"Subclasses can specify a different error class, but it should be a subclass of `RPCError`." |
|
104 |
|
|
105 |
def __init__(self, raw): |
|
106 |
self._raw = raw |
|
107 |
self._parsed = False |
|
108 |
self._root = None |
|
109 |
self._errors = [] |
|
110 |
|
|
111 |
def __repr__(self): |
|
112 |
return self._raw |
|
113 |
|
|
114 |
def parse(self): |
|
115 |
"Parses the *rpc-reply*." |
|
116 |
if self._parsed: return |
|
117 |
root = self._root = to_ele(self._raw) # The <rpc-reply> element |
|
118 |
# Per RFC 4741 an <ok/> tag is sent when there are no errors or warnings |
|
119 |
ok = root.find(qualify("ok")) |
|
120 |
if ok is None: |
|
121 |
# Create RPCError objects from <rpc-error> elements |
|
122 |
error = root.find(qualify("rpc-error")) |
|
123 |
if error is not None: |
|
124 |
for err in root.getiterator(error.tag): |
|
125 |
# Process a particular <rpc-error> |
|
126 |
self._errors.append(ERROR_CLS(err)) |
|
127 |
self._parsed = True |
|
128 |
|
|
129 |
@property |
|
130 |
def xml(self): |
|
131 |
"*rpc-reply* element as returned." |
|
132 |
return self._raw |
|
133 |
|
|
134 |
@property |
|
135 |
def ok(self): |
|
136 |
"Boolean value indicating if there were no errors." |
|
137 |
return not self.errors # empty list => false |
|
138 |
|
|
139 |
@property |
|
140 |
def error(self): |
|
141 |
"Returns the first `RPCError` and :const:`None` if there were no errors." |
|
142 |
self.parse() |
|
143 |
if self._errors: |
|
144 |
return self._errors[0] |
|
145 |
else: |
|
146 |
return None |
|
147 |
|
|
148 |
@property |
|
149 |
def errors(self): |
|
150 |
"""`list` of `RPCError` objects. Will be empty if there were no *rpc-error* elements in |
|
151 |
reply.""" |
|
152 |
self.parse() |
|
153 |
return self._errors |
|
154 |
|
|
155 |
|
|
164 | 156 |
class RPCReplyListener(SessionListener): # internal use |
165 | 157 |
|
166 | 158 |
creation_lock = Lock() |
... | ... | |
212 | 204 |
|
213 | 205 |
|
214 | 206 |
class RPC(object): |
215 |
|
|
216 |
"""Base class for all operations. |
|
217 |
|
|
218 |
Directly corresponds to *<rpc>* requests. Handles making the request, and |
|
219 |
taking delivery of the reply. |
|
220 |
""" |
|
221 |
|
|
222 |
#: Subclasses can specify their dependencies on capabilities. List of URI's |
|
223 |
# or abbreviated names, e.g. ':writable-running'. These are verified at the |
|
224 |
# time of object creation. If the capability is not available, a |
|
225 |
# :exc:`MissingCapabilityError` is raised. |
|
207 |
|
|
208 |
"""Base class for all operations, directly corresponding to *rpc* requests. Handles making the |
|
209 |
request, and taking delivery of the reply.""" |
|
210 |
|
|
226 | 211 |
DEPENDS = [] |
227 |
|
|
228 |
#: Subclasses can specify a different reply class, but it must be a |
|
229 |
# subclass of :class:`RPCReply`. |
|
212 |
"""Subclasses can specify their dependencies on capabilities. List of URI's or abbreviated |
|
213 |
names, e.g. ':writable-running'. These are verified at the time of instantiation. If the |
|
214 |
capability is not available, a :exc:`MissingCapabilityError` is raised. |
|
215 |
""" |
|
216 |
|
|
230 | 217 |
REPLY_CLS = RPCReply |
231 |
|
|
218 |
"Subclasses can specify a different reply class, but it should be a subclass of `RPCReply`." |
|
219 |
|
|
232 | 220 |
def __init__(self, session, async=False, timeout=None, raise_mode="none"): |
233 | 221 |
self._session = session |
234 | 222 |
try: |
... | ... | |
239 | 227 |
self._async = async |
240 | 228 |
self._timeout = timeout |
241 | 229 |
self._raise_mode = raise_mode |
242 |
self._id = uuid1().urn # Keeps things simple instead of having a class attr that has to be locked |
|
230 |
self._id = uuid1().urn # Keeps things simple instead of having a class attr with running ID that has to be locked
|
|
243 | 231 |
self._listener = RPCReplyListener(session) |
244 | 232 |
self._listener.register(self._id, self) |
245 | 233 |
self._reply = None |
246 | 234 |
self._error = None |
247 | 235 |
self._event = Event() |
248 |
|
|
249 |
def _build(self, subele):
|
|
250 |
# internal |
|
236 |
|
|
237 |
def _wrap(self, subele):
|
|
238 |
# internal use
|
|
251 | 239 |
ele = new_ele("rpc", {"message-id": self._id}, xmlns=BASE_NS_1_0) |
252 | 240 |
ele.append(subele) |
253 | 241 |
return to_xml(ele) |
254 | 242 |
|
255 | 243 |
def _request(self, op): |
256 |
"""Subclasses call this method to make the RPC request. |
|
244 |
"""Implementations of :meth:`request` call this method to send the request and process the |
|
245 |
reply. |
|
257 | 246 |
|
258 |
In synchronous mode, waits until the reply is received and returns |
|
259 |
:class:`RPCReply`. |
|
247 |
In synchronous mode, blocks until the reply is received and returns `RPCReply`. Depending on |
|
248 |
the :attr:`raise_mode` a *rpc-error* element in the reply may lead to an :exc:`RPCError` |
|
249 |
exception. |
|
260 | 250 |
|
261 |
In asynchronous mode, returns immediately, returning a reference to this
|
|
262 |
object. The :attr:`event` attribute will be set when the reply has been
|
|
263 |
received (see :attr:`reply`) or an error occured (see :attr:`error`).
|
|
251 |
In asynchronous mode, returns immediately, returning *self*. The :attr:`event` attribute
|
|
252 |
will be set when the reply has been received (see :attr:`reply`) or an error occured (see
|
|
253 |
:attr:`error`). |
|
264 | 254 |
|
265 |
:type opspec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element` |
|
266 |
:rtype: :class:`RPCReply` (sync) or :class:`RPC` (async) |
|
255 |
:param op: operation to be requested |
|
256 |
:type ops: `~xml.etree.ElementTree.Element` |
|
257 |
|
|
258 |
:rtype: `RPCReply` (sync) or `RPC` (async) |
|
267 | 259 |
""" |
268 | 260 |
logger.info('Requesting %r' % self.__class__.__name__) |
269 |
req = self._build(op)
|
|
261 |
req = self._wrap(op)
|
|
270 | 262 |
self._session.send(req) |
271 | 263 |
if self._async: |
272 | 264 |
logger.debug('Async request, returning %r', self) |
... | ... | |
292 | 284 |
raise TimeoutExpiredError |
293 | 285 |
|
294 | 286 |
def request(self, *args, **kwds): |
295 |
"Subclasses implement this method." |
|
296 |
return self._request(self.SPEC) |
|
287 |
"""Subclasses must implement this method. Typically only the request needs to be built as an |
|
288 |
`~xml.etree.ElementTree.Element` and everything else can be handed off to |
|
289 |
:meth:`_request`.""" |
|
290 |
pass |
|
297 | 291 |
|
298 | 292 |
def _assert(self, capability): |
299 |
"""Subclasses can use this method to verify that a capability is available |
|
300 |
with the NETCONF server, before making a request that requires it. A |
|
301 |
:exc:`MissingCapabilityError` will be raised if the capability is not |
|
302 |
available.""" |
|
293 |
"""Subclasses can use this method to verify that a capability is available with the NETCONF |
|
294 |
server, before making a request that requires it. A :exc:`MissingCapabilityError` will be |
|
295 |
raised if the capability is not available.""" |
|
303 | 296 |
if capability not in self._session.server_capabilities: |
304 | 297 |
raise MissingCapabilityError('Server does not support [%s]' % |
305 | 298 |
capability) |
... | ... | |
313 | 306 |
# internal use |
314 | 307 |
self._error = err |
315 | 308 |
self._event.set() |
316 |
|
|
309 |
|
|
317 | 310 |
@property |
318 | 311 |
def reply(self): |
319 |
":class:`RPCReply` element if reply has been received or :const:`None`"
|
|
312 |
"`RPCReply` element if reply has been received or :const:`None`" |
|
320 | 313 |
return self._reply |
321 |
|
|
314 |
|
|
322 | 315 |
@property |
323 | 316 |
def error(self): |
324 | 317 |
""":exc:`Exception` type if an error occured or :const:`None`. |
325 | 318 |
|
326 | 319 |
.. note:: |
327 |
This represents an error which prevented a reply from being |
|
328 |
received. An *<rpc-error>* does not fall in that category -- see |
|
329 |
:class:`RPCReply` for that. |
|
320 |
This represents an error which prevented a reply from being received. An *rpc-error* |
|
321 |
does not fall in that category -- see `RPCReply` for that. |
|
330 | 322 |
""" |
331 | 323 |
return self._error |
332 |
|
|
324 |
|
|
333 | 325 |
@property |
334 | 326 |
def id(self): |
335 |
"The *message-id* for this RPC" |
|
327 |
"The *message-id* for this RPC."
|
|
336 | 328 |
return self._id |
337 |
|
|
329 |
|
|
338 | 330 |
@property |
339 | 331 |
def session(self): |
340 |
"""The :class:`~ncclient.transport.Session` object associated with this |
|
341 |
RPC""" |
|
332 |
"The `~ncclient.transport.Session` object associated with this RPC." |
|
342 | 333 |
return self._session |
343 | 334 |
|
344 | 335 |
@property |
345 | 336 |
def event(self): |
346 |
""":class:`~threading.Event` that is set when reply has been received or |
|
347 |
error occured.""" |
|
337 |
"""`~threading.Event` that is set when reply has been received or when an error preventing |
|
338 |
delivery of the reply occurs. |
|
339 |
""" |
|
348 | 340 |
return self._event |
349 | 341 |
|
350 | 342 |
def set_async(self, async=True): |
351 |
"""Set asynchronous mode for this RPC.""" |
|
352 | 343 |
self._async = async |
353 | 344 |
if async and not session.can_pipeline: |
354 | 345 |
raise UserWarning('Asynchronous mode not supported for this device/session') |
... | ... | |
358 | 349 |
self._raise_mode = mode |
359 | 350 |
|
360 | 351 |
def set_timeout(self, timeout): |
361 |
"""Set the timeout for synchronous waiting; defining how long the RPC |
|
362 |
request will block on a reply before raising an error. Irrelevant for |
|
363 |
asynchronous usage.""" |
|
364 | 352 |
self._timeout = timeout |
365 | 353 |
|
366 |
#: Whether this RPC is asynchronous |
|
354 |
raise_mode = property(fget=lambda self: self._raise_mode, fset=set_raise_mode) |
|
355 |
"""Depending on this exception raising mode, an *rpc-error* in the reply may be raised as |
|
356 |
:exc:`RPCError` exceptions. Valid values: |
|
357 |
|
|
358 |
* ``"all"`` -- any kind of *rpc-error* (error or warning) |
|
359 |
* ``"errors"`` -- when the *error-type* element says it is an error |
|
360 |
* ``"none"`` -- neither |
|
361 |
""" |
|
362 |
|
|
367 | 363 |
is_async = property(fget=lambda self: self._async, fset=set_async) |
368 |
|
|
369 |
#: Timeout for synchronous waiting |
|
364 |
"""Specifies whether this RPC will be / was requested asynchronously. By default RPC's are |
|
365 |
synchronous. |
|
366 |
""" |
|
367 |
|
|
370 | 368 |
timeout = property(fget=lambda self: self._timeout, fset=set_timeout) |
369 |
"""Timeout in seconds for synchronous waiting defining how long the RPC request will block on a |
|
370 |
reply before raising :exc:`TimeoutExpiredError`. By default there is no timeout, represented by |
|
371 |
:const:`None`. |
|
372 |
|
|
373 |
Irrelevant for asynchronous usage. |
|
374 |
""" |
b/ncclient/transport/ssh.py | ||
---|---|---|
25 | 25 |
from session import Session |
26 | 26 |
|
27 | 27 |
import logging |
28 |
logger = logging.getLogger('ncclient.transport.ssh')
|
|
28 |
logger = logging.getLogger("ncclient.transport.ssh")
|
|
29 | 29 |
|
30 | 30 |
BUF_SIZE = 4096 |
31 | 31 |
MSG_DELIM = "]]>]]>" |
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 |
|
36 |
acceptable, and :const:`False` if not.
|
|
35 |
"""An `unknown host callback` returns :const:`True` if it finds the key acceptable, and
|
|
36 |
:const:`False` if not. |
|
37 | 37 |
|
38 |
This default callback always returns :const:`False`, which would lead to |
|
39 |
:meth:`connect` raising a :exc:`SSHUnknownHost` exception. |
|
40 |
|
|
41 |
Supply another valid callback if you need to verify the host key |
|
42 |
programatically. |
|
38 |
This default callback always returns :const:`False`, which would lead to :meth:`connect` raising |
|
39 |
a :exc:`SSHUnknownHost` exception. |
|
40 |
|
|
41 |
Supply another valid callback if you need to verify the host key programatically. |
|
43 | 42 |
|
44 | 43 |
:arg host: the hostname that needs to be verified |
45 |
:type host: string
|
|
44 |
:type host: `string`
|
|
46 | 45 |
|
47 |
:arg fingerprint: a hex string representing the host key fingerprint |
|
48 |
:type fingerprint: string
|
|
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`
|
|
49 | 48 |
""" |
50 | 49 |
return False |
51 | 50 |
|
... | ... | |
71 | 70 |
self._parsing_pos = 0 |
72 | 71 |
|
73 | 72 |
def _parse(self): |
74 |
'''Messages ae delimited by MSG_DELIM. The buffer could have grown by a
|
|
73 |
"""Messages ae delimited by MSG_DELIM. The buffer could have grown by a
|
|
75 | 74 |
maximum of BUF_SIZE bytes everytime this method is called. Retains state |
76 | 75 |
across method calls and if a byte has been read it will not be |
77 |
considered again. ''' |
|
76 |
considered again. |
|
77 |
""" |
|
78 | 78 |
delim = MSG_DELIM |
79 | 79 |
n = len(delim) - 1 |
80 | 80 |
expect = self._parsing_state |
... | ... | |
115 | 115 |
self._parsing_pos = self._buffer.tell() |
116 | 116 |
|
117 | 117 |
def load_known_hosts(self, filename=None): |
118 |
"""Load host keys from a :file:`known_hosts`-style file. Can be called multiple |
|
119 |
times. |
|
118 |
"""Load host keys from a :file:`known_hosts`-style file. Can be called multiple times. |
|
120 | 119 |
|
121 | 120 |
If *filename* is not specified, looks in the default locations i.e. |
122 | 121 |
:file:`~/.ssh/known_hosts` and :file:`~/ssh/known_hosts` for Windows. |
... | ... | |
144 | 143 |
unknown_host_cb=default_unknown_host_cb, |
145 | 144 |
username=None, password=None, |
146 | 145 |
key_filename=None, allow_agent=True, look_for_keys=True): |
147 |
"""Connect via SSH and initialize the NETCONF session. First attempts |
|
148 |
the publickey authentication method and then password authentication.
|
|
146 |
"""Connect via SSH and initialize the NETCONF session. First attempts the publickey
|
|
147 |
authentication method and then password authentication. |
|
149 | 148 |
|
150 |
To disable attemting publickey authentication altogether, call with |
|
151 |
*allow_agent* and *look_for_keys* as :const:`False`.
|
|
149 |
To disable attemting publickey authentication altogether, call with *allow_agent* and
|
|
150 |
*look_for_keys* as :const:`False`. |
|
152 | 151 |
|
153 | 152 |
:arg host: the hostname or IP address to connect to |
154 | 153 |
:type host: `string` |
... | ... | |
156 | 155 |
:arg port: by default 830, but some devices use the default SSH port of 22 so this may need to be specified |
157 | 156 |
:type port: `int` |
158 | 157 |
|
159 |
:arg timeout: an optional timeout for the TCP handshake
|
|
158 |
:arg timeout: an optional timeout for socket connect
|
|
160 | 159 |
:type timeout: `int` |
161 | 160 |
|
162 |
:arg unknown_host_cb: called when a host key is not recognized
|
|
161 |
:arg unknown_host_cb: called when the server host key is not recognized
|
|
163 | 162 |
:type unknown_host_cb: see :meth:`signature <ssh.default_unknown_host_cb>` |
164 | 163 |
|
165 | 164 |
:arg username: the username to use for SSH authentication |
Also available in: Unified diff