Revision dd225c7a
b/ncclient/capabilities.py | ||
---|---|---|
12 | 12 |
# See the License for the specific language governing permissions and |
13 | 13 |
# limitations under the License. |
14 | 14 |
|
15 |
_capability_map = { |
|
16 |
'urn:liberouter:params:netconf:capability:power-control:1.0': |
|
17 |
[':power-control', ':power-control:1.0'] |
|
18 |
} |
|
19 |
|
|
15 | 20 |
def _abbreviate(uri): |
16 | 21 |
if uri.startswith('urn:ietf:params:netconf:'): |
17 | 22 |
splitted = uri.split(':') |
... | ... | |
19 | 24 |
return [ ':' + splitted[5], ':' + splitted[5] + ':' + splitted[6] ] |
20 | 25 |
elif ':base:' in uri: |
21 | 26 |
return [ ':base', ':base' + ':'+ splitted[5] ] |
22 |
else: |
|
23 |
return [] |
|
24 |
else: |
|
25 |
return [] |
|
27 |
elif uri in _capability_map: |
|
28 |
return _capability_map[uri] |
|
29 |
return [] |
|
26 | 30 |
|
27 | 31 |
def schemes(url_uri): |
28 | 32 |
"""Given a URI that has a *scheme* query string (i.e. *:url* capability |
... | ... | |
87 | 91 |
""" |
88 | 92 |
return key in self |
89 | 93 |
|
90 |
def get_uris(self, shorthand): |
|
91 |
return [uri for uri, abbrs in self._dict.items() if shorthand in abbrs] |
|
94 |
def get_uri(self, shorthand): |
|
95 |
for uri, abbrs in self._dict.items(): |
|
96 |
if shorthand in abbrs: |
|
97 |
return uri |
|
92 | 98 |
|
93 | 99 |
#: :class:`Capabilities` object representing the capabilities currently supported by NCClient |
94 | 100 |
CAPABILITIES = Capabilities([ |
... | ... | |
101 | 107 |
'urn:ietf:params:netconf:capability:url:1.0?scheme=http,ftp,file,https,sftp', |
102 | 108 |
'urn:ietf:params:netconf:capability:validate:1.0', |
103 | 109 |
'urn:ietf:params:netconf:capability:xpath:1.0', |
110 |
'urn:liberouter:params:netconf:capability:power-control:1.0' |
|
104 | 111 |
#'urn:ietf:params:netconf:capability:notification:1.0', # TODO |
105 | 112 |
#'urn:ietf:params:netconf:capability:interleave:1.0' # theoretically already supported |
106 | 113 |
]) |
b/ncclient/manager.py | ||
---|---|---|
23 | 23 |
|
24 | 24 |
def connect_ssh(*args, **kwds): |
25 | 25 |
"""Connect to NETCONF server over SSH. See :meth:`SSHSession.connect() |
26 |
<ncclient.transport.SSHSession.connect>` for function signature.""" |
|
26 |
<ncclient.transport.SSHSession.connect>` for argument details. |
|
27 |
|
|
28 |
:rtype: :class:`Manager` |
|
29 |
""" |
|
27 | 30 |
session = transport.SSHSession(capabilities.CAPABILITIES) |
28 | 31 |
session.load_known_hosts() |
29 | 32 |
session.connect(*args, **kwds) |
... | ... | |
32 | 35 |
#: Same as :meth:`connect_ssh` |
33 | 36 |
connect = connect_ssh |
34 | 37 |
|
35 |
#: Raise all :class:`~ncclient.operations.rpc.RPCError` |
|
36 |
RAISE_ALL = 0 |
|
37 |
#: Only raise when *error-severity* is "error" i.e. no warnings |
|
38 |
RAISE_ERR = 1 |
|
39 |
#: Don't raise any |
|
40 |
RAISE_NONE = 2 |
|
41 |
|
|
42 |
class Manager: |
|
38 |
class Manager(object): |
|
43 | 39 |
|
44 |
"""API for NETCONF operations. Currently only supports making synchronous |
|
45 |
RPC requests. |
|
40 |
"""API for NETCONF operations. |
|
46 | 41 |
|
47 | 42 |
It is also a context manager, so a :class:`Manager` instance can be used |
48 | 43 |
with the *with* statement. The session is closed when the context ends. """ |
49 | 44 |
|
50 | 45 |
def __init__(self, session): |
51 | 46 |
self._session = session |
52 |
self._raise = RAISE_ALL |
|
53 |
|
|
54 |
def set_rpc_error_action(self, action): |
|
55 |
"""Specify the action to take when an *<rpc-error>* element is encountered. |
|
56 |
|
|
57 |
:arg action: one of :attr:`RAISE_ALL`, :attr:`RAISE_ERR`, :attr:`RAISE_NONE` |
|
58 |
""" |
|
59 |
self._raise = action |
|
60 | 47 |
|
61 | 48 |
def __enter__(self): |
62 | 49 |
return self |
... | ... | |
65 | 52 |
self.close() |
66 | 53 |
return False |
67 | 54 |
|
68 |
def do(self, op, *args, **kwds): |
|
69 |
op = operations.OPERATIONS[op](self._session) |
|
70 |
reply = op.request(*args, **kwds) |
|
71 |
if not reply.ok: |
|
72 |
if self._raise == RAISE_ALL: |
|
73 |
raise reply.error |
|
74 |
elif self._raise == RAISE_ERR: |
|
75 |
for error in reply.errors: |
|
76 |
if error.severity == 'error': |
|
77 |
raise error |
|
78 |
return reply |
|
79 |
|
|
80 |
#: :see: :meth:`Get.request() <ncclient.operations.Get.request>` |
|
81 |
get = lambda self, *args, **kwds: self.do('get', *args, **kwds) |
|
82 |
|
|
83 |
#: :see: :meth:`GetConfig.request() <ncclient.operations.GetConfig.request>` |
|
84 |
get_config = lambda self, *args, **kwds: self.do('get-config', *args, **kwds) |
|
85 |
|
|
86 |
#: :see: :meth:`EditConfig.request() <ncclient.operations.EditConfig.request>` |
|
87 |
edit_config = lambda self, *args, **kwds: self.do('edit-config', *args, **kwds) |
|
88 |
|
|
89 |
#: :see: :meth:`CopyConfig.request() <ncclient.operations.CopyConfig.request>` |
|
90 |
copy_config = lambda self, *args, **kwds: self.do('copy-config', *args, **kwds) |
|
91 |
|
|
92 |
#: :see: :meth:`GetConfig.request() <ncclient.operations.Validate.request>` |
|
93 |
validate = lambda self, *args, **kwds: self.do('validate', *args, **kwds) |
|
94 |
|
|
95 |
#: :see: :meth:`Commit.request() <ncclient.operations.Commit.request>` |
|
96 |
commit = lambda self, *args, **kwds: self.do('commit', *args, **kwds) |
|
97 |
|
|
98 |
#: :see: :meth:`DiscardChanges.request() <ncclient.operations.DiscardChanges.request>` |
|
99 |
discard_changes = lambda self, *args, **kwds: self.do('discard-changes', *args, **kwds) |
|
100 |
|
|
101 |
#: :see: :meth:`DeleteConfig.request() <ncclient.operations.DeleteConfig.request>` |
|
102 |
delete_config = lambda self, *args, **kwds: self.do('delete-config', *args, **kwds) |
|
103 |
|
|
104 |
#: :see: :meth:`Lock.request() <ncclient.operations.Lock.request>` |
|
105 |
lock = lambda self, *args, **kwds: self.do('lock', *args, **kwds) |
|
106 |
|
|
107 |
#: :see: :meth:`DiscardChanges.request() <ncclient.operations.Unlock.request>` |
|
108 |
unlock = lambda self, *args, **kwds: self.do('unlock', *args, **kwds) |
|
109 |
|
|
110 |
#: :see: :meth:`CloseSession.request() <ncclient.operations.CloseSession.request>` |
|
111 |
close_session = lambda self, *args, **kwds: self.do('close-session', *args, **kwds) |
|
112 |
|
|
113 |
#: :see: :meth:`KillSession.request() <ncclient.operations.KillSession.request>` |
|
114 |
kill_session = lambda self, *args, **kwds: self.do('kill-session', *args, **kwds) |
|
55 |
def __getattr__(self, name): |
|
56 |
try: |
|
57 |
return operations.INDEX[name](self.session).request |
|
58 |
except KeyError: |
|
59 |
raise AttributeError |
|
115 | 60 |
|
116 | 61 |
def locked(self, target): |
117 | 62 |
"""Returns a context manager for the *with* statement. |
b/ncclient/operations/__init__.py | ||
---|---|---|
14 | 14 |
|
15 | 15 |
from errors import OperationError, TimeoutExpiredError, MissingCapabilityError |
16 | 16 |
from rpc import RPC, RPCReply, RPCError |
17 |
|
|
18 |
# rfc4741 ops |
|
17 | 19 |
from retrieve import Get, GetConfig, GetReply |
18 | 20 |
from edit import EditConfig, CopyConfig, DeleteConfig, Validate, Commit, DiscardChanges |
19 | 21 |
from session import CloseSession, KillSession |
20 | 22 |
from lock import Lock, Unlock, LockContext |
21 |
#from subscribe import CreateSubscription |
|
23 |
# others... |
|
24 |
from flowmon import PoweroffMachine, RebootMachine |
|
22 | 25 |
|
23 |
OPERATIONS = {
|
|
26 |
INDEX = {
|
|
24 | 27 |
'get': Get, |
25 |
'get-config': GetConfig,
|
|
26 |
'edit-config': EditConfig,
|
|
27 |
'copy-config': CopyConfig,
|
|
28 |
'get_config': GetConfig,
|
|
29 |
'edit_config': EditConfig,
|
|
30 |
'copy_config': CopyConfig,
|
|
28 | 31 |
'validate': Validate, |
29 | 32 |
'commit': Commit, |
30 |
'discard-changes': DiscardChanges,
|
|
31 |
'delete-config': DeleteConfig,
|
|
33 |
'discard_changes': DiscardChanges,
|
|
34 |
'delete_config': DeleteConfig,
|
|
32 | 35 |
'lock': Lock, |
33 | 36 |
'unlock': Unlock, |
34 |
'close-session': CloseSession, |
|
35 |
'kill-session': KillSession, |
|
37 |
'close_session': CloseSession, |
|
38 |
'kill_session': KillSession, |
|
39 |
'poweroff_machine': PoweroffMachine, |
|
40 |
'reboot_machine': RebootMachine |
|
36 | 41 |
} |
37 | 42 |
|
38 | 43 |
__all__ = [ |
... | ... | |
51 | 56 |
'DeleteConfig', |
52 | 57 |
'Lock', |
53 | 58 |
'Unlock', |
59 |
'PoweroffMachine', |
|
60 |
'RebootMachine', |
|
54 | 61 |
'LockContext', |
55 | 62 |
'CloseSession', |
56 | 63 |
'KillSession', |
b/ncclient/operations/edit.py | ||
---|---|---|
27 | 27 |
|
28 | 28 |
class EditConfig(RPC): |
29 | 29 |
|
30 |
# TESTED |
|
31 |
|
|
32 | 30 |
"*<edit-config>* RPC" |
33 | 31 |
|
34 | 32 |
SPEC = {'tag': 'edit-config', 'subtree': []} |
... | ... | |
79 | 77 |
|
80 | 78 |
class DeleteConfig(RPC): |
81 | 79 |
|
82 |
# TESTED |
|
83 |
|
|
84 | 80 |
"*<delete-config>* RPC" |
85 | 81 |
|
86 | 82 |
SPEC = {'tag': 'delete-config', 'subtree': []} |
... | ... | |
99 | 95 |
|
100 | 96 |
class CopyConfig(RPC): |
101 | 97 |
|
102 |
# TESTED |
|
103 |
|
|
104 | 98 |
"*<copy-config>* RPC" |
105 | 99 |
|
106 | 100 |
SPEC = {'tag': 'copy-config', 'subtree': []} |
... | ... | |
123 | 117 |
|
124 | 118 |
class Validate(RPC): |
125 | 119 |
|
126 |
# TESTED |
|
127 |
|
|
128 | 120 |
"*<validate>* RPC. Depends on the *:validate* capability." |
129 | 121 |
|
130 | 122 |
DEPENDS = [':validate'] |
... | ... | |
153 | 145 |
|
154 | 146 |
class Commit(RPC): |
155 | 147 |
|
156 |
# TESTED |
|
157 |
|
|
158 | 148 |
"*<commit>* RPC. Depends on the *:candidate* capability." |
159 | 149 |
|
160 | 150 |
DEPENDS = [':candidate'] |
b/ncclient/operations/lock.py | ||
---|---|---|
16 | 16 |
|
17 | 17 |
from copy import deepcopy |
18 | 18 |
|
19 |
from rpc import RPC, RPCReply, RPCError
|
|
19 |
from rpc import RPC |
|
20 | 20 |
|
21 |
#class LockReply(RPCReply): |
|
22 |
# |
|
23 |
# ERROR_CLS = LockDeniedError |
|
24 |
# |
|
25 |
#class LockDeniedError(RPCError): |
|
26 |
# |
|
27 |
# def __new__(cls, err_dict): |
|
28 |
# if rpcerr['tag'] != 'lock-denied': |
|
29 |
# return RPCError(err_dict) |
|
30 |
# else: |
|
31 |
# return object.__new__(LockDeniedError) |
|
32 |
# |
|
33 |
# def __init__(self, err_dict): |
|
34 |
# RPCError.__init__(self, err_dict) |
|
21 |
# TODO: |
|
22 |
# should have some way to parse session-id from a lock-denied error |
|
35 | 23 |
|
36 | 24 |
class Lock(RPC): |
37 | 25 |
|
38 |
# TESTED |
|
39 |
|
|
40 | 26 |
"*<lock>* RPC" |
41 | 27 |
|
42 | 28 |
SPEC = { |
... | ... | |
63 | 49 |
|
64 | 50 |
class Unlock(RPC): |
65 | 51 |
|
66 |
# TESTED |
|
67 |
|
|
68 | 52 |
"*<unlock>* RPC" |
69 | 53 |
|
70 | 54 |
SPEC = { |
... | ... | |
89 | 73 |
|
90 | 74 |
class LockContext: |
91 | 75 |
|
92 |
# TESTED |
|
93 |
|
|
94 | 76 |
""" |
95 | 77 |
A context manager for the :class:`Lock` / :class:`Unlock` pair of RPC's. |
96 | 78 |
|
b/ncclient/operations/retrieve.py | ||
---|---|---|
21 | 21 |
|
22 | 22 |
class GetReply(RPCReply): |
23 | 23 |
|
24 |
# TESTED |
|
25 |
|
|
26 | 24 |
"""Adds attributes for the *<data>* element to :class:`RPCReply`, which |
27 | 25 |
pertains to the :class:`Get` and :class:`GetConfig` operations.""" |
28 | 26 |
|
29 | 27 |
def _parsing_hook(self, root): |
30 | 28 |
self._data = None |
31 | 29 |
if not self._errors: |
32 |
self._data = xml_.find(root, 'data', |
|
33 |
nslist=[xml_.BASE_NS, |
|
34 |
xml_.CISCO_BS]) |
|
30 |
self._data = xml_.find(root, 'data', nslist=xml_.NSLIST) |
|
35 | 31 |
|
36 | 32 |
@property |
37 | 33 |
def data_ele(self): |
... | ... | |
58 | 54 |
|
59 | 55 |
class Get(RPC): |
60 | 56 |
|
61 |
# TESTED |
|
62 |
|
|
63 | 57 |
"The *<get>* RPC" |
64 | 58 |
|
65 | 59 |
SPEC = {'tag': 'get', 'subtree': []} |
... | ... | |
80 | 74 |
|
81 | 75 |
class GetConfig(RPC): |
82 | 76 |
|
83 |
# TESTED |
|
84 |
|
|
85 | 77 |
"The *<get-config>* RPC" |
86 | 78 |
|
87 | 79 |
SPEC = {'tag': 'get-config', 'subtree': []} |
b/ncclient/operations/rpc.py | ||
---|---|---|
57 | 57 |
return |
58 | 58 |
root = self._root = xml_.xml2ele(self._raw) # <rpc-reply> element |
59 | 59 |
# per rfc 4741 an <ok/> tag is sent when there are no errors or warnings |
60 |
ok = xml_.find(root, 'ok', nslist=[xml_.BASE_NS, xml_.CISCO_BS])
|
|
60 |
ok = xml_.find(root, 'ok', nslist=xml_.NSLIST)
|
|
61 | 61 |
if ok is not None: |
62 | 62 |
logger.debug('parsed [%s]' % ok.tag) |
63 | 63 |
else: # create RPCError objects from <rpc-error> elements |
64 |
error = xml_.find(root, 'rpc-error', nslist=[xml_.BASE_NS, xml_.CISCO_BS])
|
|
64 |
error = xml_.find(root, 'rpc-error', nslist=xml_.NSLIST)
|
|
65 | 65 |
if error is not None: |
66 | 66 |
logger.debug('parsed [%s]' % error.tag) |
67 | 67 |
for err in root.getiterator(error.tag): |
... | ... | |
272 | 272 |
spec = { |
273 | 273 |
'tag': 'rpc', |
274 | 274 |
'attrib': { |
275 |
'xmlns': xml_.BASE_NS, |
|
275 |
'xmlns': xml_.BASE_NS_1_0,
|
|
276 | 276 |
'message-id': self._id |
277 | 277 |
}, |
278 | 278 |
'subtree': [ opspec ] |
... | ... | |
314 | 314 |
def request(self, *args, **kwds): |
315 | 315 |
"""Subclasses implement this method. Here, the operation is constructed |
316 | 316 |
in :ref:`dtree`, and the result of :meth:`_request` returned.""" |
317 |
raise NotImplementedError
|
|
317 |
return self._request(self.SPEC)
|
|
318 | 318 |
|
319 | 319 |
def _delivery_hook(self): |
320 | 320 |
"""Subclasses can implement this method. Will be called after |
b/ncclient/operations/session.py | ||
---|---|---|
20 | 20 |
|
21 | 21 |
class CloseSession(RPC): |
22 | 22 |
|
23 |
# TESTED |
|
24 |
|
|
25 | 23 |
"*<close-session>* RPC. The connection to NETCONF server is also closed." |
26 | 24 |
|
27 | 25 |
SPEC = { 'tag': 'close-session' } |
... | ... | |
29 | 27 |
def _delivery_hook(self): |
30 | 28 |
self.session.close() |
31 | 29 |
|
32 |
def request(self): |
|
33 |
":seealso: :ref:`return`" |
|
34 |
return self._request(CloseSession.SPEC) |
|
35 |
|
|
36 | 30 |
|
37 | 31 |
class KillSession(RPC): |
38 | 32 |
|
b/ncclient/operations/subscribe.py | ||
---|---|---|
12 | 12 |
# See the License for the specific language governing permissions and |
13 | 13 |
# limitations under the License. |
14 | 14 |
|
15 |
from rpc import RPC |
|
15 |
class Notification: |
|
16 |
pass |
|
16 | 17 |
|
17 |
#from ncclient.xml import qualify as _ |
|
18 |
#from ncclient.transport import SessionListener |
|
19 |
# |
|
20 |
#NOTIFICATION_NS = 'urn:ietf:params:xml:ns:netconf:notification:1.0' |
|
21 |
# |
|
22 |
## TODO when can actually test it... |
|
23 |
# |
|
24 |
#class CreateSubscription(RPC): |
|
25 |
# # tested: no |
|
26 |
# |
|
27 |
# SPEC = { |
|
28 |
# 'tag': _('create-subscription', NOTIFICATION_NS) |
|
29 |
# } |
|
30 |
# |
|
31 |
#class Notification: pass |
|
32 |
# |
|
33 |
#class NotificationListener(SessionListener): pass |
|
18 |
class CreateSubscription: |
|
19 |
pass |
|
20 |
|
|
21 |
class NotificationListener: |
|
22 |
pass |
b/ncclient/transport/session.py | ||
---|---|---|
227 | 227 |
"Given a list of capability URI's returns <hello> message XML string" |
228 | 228 |
spec = { |
229 | 229 |
'tag': 'hello', |
230 |
'attrib': {'xmlns': xml_.BASE_NS}, |
|
230 |
'attrib': {'xmlns': xml_.BASE_NS_1_0},
|
|
231 | 231 |
'subtree': [{ |
232 | 232 |
'tag': 'capabilities', |
233 | 233 |
'subtree': # this is fun :-) |
b/ncclient/xml_.py | ||
---|---|---|
30 | 30 |
### Namespace-related |
31 | 31 |
|
32 | 32 |
#: Base NETCONF namespace |
33 |
BASE_NS = 'urn:ietf:params:xml:ns:netconf:base:1.0' |
|
33 |
BASE_NS_1_0 = 'urn:ietf:params:xml:ns:netconf:base:1.0'
|
|
34 | 34 |
#: ... and this is BASE_NS according to Cisco devices tested |
35 |
CISCO_BS = 'urn:ietf:params:netconf:base:1.0' |
|
35 |
CISCO_BS_1_0 = 'urn:ietf:params:netconf:base:1.0'
|
|
36 | 36 |
#: namespace for Tail-f data model |
37 | 37 |
TAILF_AAA_1_1 = 'http://tail-f.com/ns/aaa/1.1' |
38 | 38 |
#: namespace for Tail-f data model |
39 | 39 |
TAILF_EXECD_1_1 = 'http://tail-f.com/ns/execd/1.1' |
40 | 40 |
#: namespace for Cisco data model |
41 |
CISCO_CPI_10 = 'http://www.cisco.com/cpi_10/schema' |
|
41 |
CISCO_CPI_1_0 = 'http://www.cisco.com/cpi_10/schema' |
|
42 |
#: namespace for Flowmon data model |
|
43 |
FLOWMON_1_0 = 'http://www.liberouter.org/ns/netopeer/flowmon/1.0' |
|
42 | 44 |
|
43 | 45 |
try: |
44 | 46 |
register_namespace = ET.register_namespace |
... | ... | |
48 | 50 |
# cElementTree uses ElementTree's _namespace_map, so that's ok |
49 | 51 |
ElementTree._namespace_map[uri] = prefix |
50 | 52 |
|
51 |
register_namespace('netconf', BASE_NS) |
|
52 |
register_namespace('aaa', TAILF_AAA_1_1) |
|
53 |
register_namespace('execd', TAILF_EXECD_1_1) |
|
54 |
register_namespace('cpi', CISCO_CPI_10) |
|
53 |
prefix_map = { |
|
54 |
BASE_NS_1_0: 'nc', |
|
55 |
TAILF_AAA_1_1: 'aaa', |
|
56 |
TAILF_EXECD_1_1: 'execd', |
|
57 |
CISCO_CPI_1_0: 'cpi', |
|
58 |
FLOWMON_1_0: 'fm', |
|
59 |
} |
|
55 | 60 |
|
61 |
for (ns, pre) in prefix_map.items(): |
|
62 |
register_namespace(pre, ns) |
|
56 | 63 |
|
57 |
qualify = lambda tag, ns=BASE_NS: tag if ns is None else '{%s}%s' % (ns, tag) |
|
64 |
qualify = lambda tag, ns=BASE_NS_1_0: tag if ns is None else '{%s}%s' % (ns, tag)
|
|
58 | 65 |
|
59 |
#: Deprecated |
|
60 |
multiqualify = lambda tag, nslist=(BASE_NS, CISCO_BS): [qualify(tag, ns) for ns in nslist] |
|
66 |
multiqualify = lambda tag, nslist=(BASE_NS_1_0, CISCO_BS_1_0): [qualify(tag, ns) for ns in nslist] |
|
61 | 67 |
|
62 | 68 |
unqualify = lambda tag: tag[tag.rfind('}')+1:] |
63 | 69 |
|
64 |
### XML with Python data structures
|
|
70 |
### XML representations
|
|
65 | 71 |
|
66 | 72 |
class DictTree: |
67 | 73 |
|
... | ... | |
169 | 175 |
|
170 | 176 |
iselement = ET.iselement |
171 | 177 |
|
178 |
|
|
179 |
NSLIST = [BASE_NS_1_0, CISCO_BS_1_0] |
|
180 |
|
|
172 | 181 |
def find(ele, tag, nslist=[]): |
173 | 182 |
"""If *nslist* is empty, same as :meth:`xml.etree.ElementTree.Element.find`. |
174 | 183 |
If it is not, *tag* is interpreted as an unqualified name and qualified |
Also available in: Unified diff