Revision a7cb58ce
b/ncclient/capabilities.py | ||
---|---|---|
14 | 14 |
|
15 | 15 |
def abbreviate(uri): |
16 | 16 |
if uri.startswith('urn:ietf:params:netconf:capability:'): |
17 |
return (':' + uri.split(':')[5]) |
|
17 |
return ':' + uri.split(':')[5] |
|
18 |
elif uri.startswith('urn:ietf:params:netconf:base:'): |
|
19 |
return ':base' |
|
18 | 20 |
|
19 |
def schemes(uri): |
|
20 |
return uri.partition("?scheme=")[2].split(',') |
|
21 |
def version(uri): |
|
22 |
if uri.startswith('urn:ietf:params:netconf:capability:'): |
|
23 |
return uri.split(':')[6] |
|
24 |
elif uri.startswith('urn:ietf:params:netconf:base:'): |
|
25 |
return uri.split(':')[5] |
|
21 | 26 |
|
22 | 27 |
class Capabilities: |
23 |
|
|
24 |
"""Represent the capabilities of client or server. Also facilitates using |
|
25 |
abbreviated capability names in addition to complete URI. |
|
26 |
""" |
|
27 |
|
|
28 |
|
|
28 | 29 |
def __init__(self, capabilities=None): |
29 | 30 |
self._dict = {} |
30 | 31 |
if isinstance(capabilities, dict): |
31 | 32 |
self._dict = capabilities |
32 | 33 |
elif isinstance(capabilities, list): |
33 | 34 |
for uri in capabilities: |
34 |
self._dict[uri] = abbreviate(uri)
|
|
35 |
|
|
35 |
self._dict[uri] = (abbreviate(uri), version(uri))
|
|
36 |
|
|
36 | 37 |
def __contains__(self, key): |
37 |
return ( key in self._dict ) or ( key in self._dict.values() ) |
|
38 |
|
|
38 |
if key in self._dict: |
|
39 |
return True |
|
40 |
for info in self._dict.values(): |
|
41 |
if key == info[0]: |
|
42 |
return True |
|
43 |
return False |
|
44 |
|
|
39 | 45 |
def __iter__(self): |
40 | 46 |
return self._dict.keys().__iter__() |
41 |
|
|
47 |
|
|
42 | 48 |
def __repr__(self): |
43 | 49 |
return repr(self._dict.keys()) |
44 |
|
|
50 |
|
|
45 | 51 |
def __list__(self): |
46 | 52 |
return self._dict.keys() |
47 |
|
|
48 |
def add(self, uri, shorthand=None):
|
|
49 |
if shorthand is None:
|
|
50 |
shorthand = abbreviate(uri)
|
|
51 |
self._dict[uri] = shorthand
|
|
52 |
|
|
53 |
|
|
54 |
def add(self, uri, info=None):
|
|
55 |
if info is None:
|
|
56 |
info = (abbreviate(uri), version(uri))
|
|
57 |
self._dict[uri] = info
|
|
58 |
|
|
53 | 59 |
set = add |
54 |
|
|
60 |
|
|
55 | 61 |
def remove(self, key): |
56 | 62 |
if key in self._dict: |
57 | 63 |
del self._dict[key] |
58 | 64 |
else: |
59 | 65 |
for uri in self._dict: |
60 |
if self._dict[uri] == key:
|
|
66 |
if key in self._dict[uri]:
|
|
61 | 67 |
del self._dict[uri] |
62 | 68 |
break |
63 | 69 |
|
64 |
# : the capabilities currently supported by ncclient |
|
70 |
def get_uri(self, shortname): |
|
71 |
for uri, info in self._dict.items(): |
|
72 |
if info[0] == shortname: |
|
73 |
return uri |
|
74 |
|
|
75 |
def url_schemes(self): |
|
76 |
url_uri = get_uri(':url') |
|
77 |
if url_uri is None: |
|
78 |
return [] |
|
79 |
else: |
|
80 |
return url_uri.partition("?scheme=")[2].split(',') |
|
81 |
|
|
82 |
def version(self, key): |
|
83 |
try: |
|
84 |
return self._dict[key][1] |
|
85 |
except KeyError: |
|
86 |
for uri, info in self._dict.items(): |
|
87 |
if info[0] == key: |
|
88 |
return info[1] |
|
89 |
|
|
90 |
|
|
91 |
#: the capabilities supported by NCClient |
|
65 | 92 |
CAPABILITIES = Capabilities([ |
66 | 93 |
'urn:ietf:params:netconf:base:1.0', |
67 | 94 |
'urn:ietf:params:netconf:capability:writable-running:1.0', |
... | ... | |
74 | 101 |
'urn:ietf:params:netconf:capability:xpath:1.0', |
75 | 102 |
#'urn:ietf:params:netconf:capability:notification:1.0', # TODO |
76 | 103 |
#'urn:ietf:params:netconf:capability:interleave:1.0' # theoretically already supported |
77 |
]) |
|
104 |
]) |
b/ncclient/content.py | ||
---|---|---|
12 | 12 |
# See the License for the specific language governing permissions and |
13 | 13 |
# limitations under the License. |
14 | 14 |
|
15 |
|
|
16 |
"""The :mod:`content` module provides methods for creating XML documents, parsing XML, and converting between different XML representations. It uses :mod:`~xml.etree.ElementTree` internally. |
|
17 |
""" |
|
18 |
|
|
15 | 19 |
from cStringIO import StringIO |
16 | 20 |
from xml.etree import cElementTree as ET |
17 | 21 |
|
... | ... | |
23 | 27 |
|
24 | 28 |
### Namespace-related |
25 | 29 |
|
26 |
# : Base NETCONf namespace
|
|
30 |
#: Base NETCONf namespace |
|
27 | 31 |
BASE_NS = 'urn:ietf:params:xml:ns:netconf:base:1.0' |
28 |
# : ... and this is BASE_NS according to Cisco devices tested
|
|
32 |
#: ... and this is BASE_NS according to Cisco devices tested |
|
29 | 33 |
CISCO_BS = 'urn:ietf:params:netconf:base:1.0' |
30 | 34 |
|
31 | 35 |
try: |
b/ncclient/manager.py | ||
---|---|---|
13 | 13 |
# limitations under the License. |
14 | 14 |
|
15 | 15 |
import capabilities |
16 |
import operations
|
|
16 |
from operations import OPERATIONS
|
|
17 | 17 |
import transport |
18 | 18 |
|
19 | 19 |
|
... | ... | |
25 | 25 |
|
26 | 26 |
connect = ssh_connect # default session type |
27 | 27 |
|
28 |
RAISE_ALL, RAISE_ERROR, RAISE_NONE = range(3) |
|
28 |
#: Raise all errors |
|
29 |
RAISE_ALL = 0 |
|
30 |
#: |
|
31 |
RAISE_ERR = 1 |
|
32 |
#: |
|
33 |
RAISE_NONE = 2 |
|
29 | 34 |
|
30 | 35 |
class Manager: |
31 | 36 |
|
... | ... | |
33 | 38 |
|
34 | 39 |
def __init__(self, session): |
35 | 40 |
self._session = session |
36 |
self._rpc_error_handling = RAISE_ALL
|
|
41 |
self._rpc_error_action = RAISE_ALL
|
|
37 | 42 |
|
38 |
def set_rpc_error_option(self, option):
|
|
43 |
def set_rpc_error_action(self, action):
|
|
39 | 44 |
self._rpc_error_handling = option |
40 | 45 |
|
41 | 46 |
def do(self, op, *args, **kwds): |
42 |
op = operations.OPERATIONS[op](self._session)
|
|
47 |
op = OPERATIONS[op](self._session) |
|
43 | 48 |
reply = op.request(*args, **kwds) |
44 | 49 |
if not reply.ok: |
45 | 50 |
if self._raise == RAISE_ALL: |
... | ... | |
59 | 64 |
|
60 | 65 |
def locked(self, target): |
61 | 66 |
"""Returns a context manager for use with the 'with' statement. |
62 |
`target` is the datastore to lock, e.g. 'candidate |
|
67 |
|
|
68 |
:arg target: name of the datastore to lock |
|
69 |
:type target: `string` |
|
63 | 70 |
""" |
64 | 71 |
return operations.LockContext(self._session, target) |
65 | 72 |
|
66 |
get = lambda self, *args, **kwds: self.do('get', *args, **kwds).data |
|
73 |
def get(self, filter=None): |
|
74 |
pass |
|
75 |
|
|
76 |
def get_config(self, source, filter=None): |
|
77 |
pass |
|
78 |
|
|
79 |
def copy_config(self, source, target): |
|
80 |
pass |
|
81 |
|
|
82 |
def validate(self, source): |
|
83 |
pass |
|
84 |
|
|
85 |
def commit(self, target): |
|
86 |
pass |
|
87 |
|
|
88 |
def discard_changes(self): |
|
89 |
pass |
|
90 |
|
|
91 |
def delete_config(self, target): |
|
92 |
pass |
|
67 | 93 |
|
68 |
get_config = lambda self, *args, **kwds: self.do('get-config', *args, **kwds).data |
|
94 |
def lock(self, target): |
|
95 |
pass |
|
69 | 96 |
|
70 |
edit_config = lambda self, *args, **kwds: self.do('edit-config', *args, **kwds) |
|
97 |
def unlock(self, target): |
|
98 |
pass |
|
71 | 99 |
|
72 |
copy_config = lambda self, *args, **kwds: self.do('copy-config', *args, **kwds) |
|
100 |
def close_session(self): |
|
101 |
pass |
|
73 | 102 |
|
74 |
validate = lambda self, *args, **kwds: self.do('validate', *args, **kwds) |
|
103 |
def kill_session(self, session_id): |
|
104 |
pass |
|
75 | 105 |
|
76 |
commit = lambda self, *args, **kwds: self.do('commit', *args, **kwds) |
|
106 |
def confirmed_commit(self, timeout=None): |
|
107 |
pass |
|
77 | 108 |
|
78 |
discard_changes = lambda self, *args, **kwds: self.do('discard-changes', *args, **kwds) |
|
109 |
def confirm(self): |
|
110 |
# give confirmation |
|
111 |
pass |
|
79 | 112 |
|
80 |
delete_config = lambda self, *args, **kwds: self.do('delete-config', *args, **kwds) |
|
113 |
def discard_changes(self): |
|
114 |
pass |
|
81 | 115 |
|
82 | 116 |
lock = lambda self, *args, **kwds: self.do('lock', *args, **kwds) |
83 | 117 |
|
... | ... | |
90 | 124 |
def close(self): |
91 | 125 |
try: # try doing it clean |
92 | 126 |
self.close_session() |
93 |
except: |
|
94 |
pass
|
|
127 |
except Exception as e:
|
|
128 |
logger.debug('error doing <close-session> -- %r' % e)
|
|
95 | 129 |
if self._session.connected: # if that didn't work... |
96 | 130 |
self._session.close() |
97 | 131 |
|
... | ... | |
99 | 133 |
def session(self, session): |
100 | 134 |
return self._session |
101 | 135 |
|
102 |
def get_capabilities(self, whose): |
|
103 |
if whose in ('manager', 'client'): |
|
104 |
return self._session._client_capabilities |
|
105 |
elif whose in ('agent', 'server'): |
|
106 |
return self._session._server_capabilities |
|
107 |
|
|
108 | 136 |
@property |
109 |
def capabilities(self): |
|
137 |
def client_capabilities(self):
|
|
110 | 138 |
return self._session._client_capabilities |
111 | 139 |
|
112 | 140 |
@property |
113 | 141 |
def server_capabilities(self): |
114 | 142 |
return self._session._server_capabilities |
143 |
|
|
144 |
@property |
|
145 |
def session_id(self): |
|
146 |
return self._session.id |
b/ncclient/operations/__init__.py | ||
---|---|---|
12 | 12 |
# See the License for the specific language governing permissions and |
13 | 13 |
# limitations under the License. |
14 | 14 |
|
15 |
'NETCONF protocol operations' |
|
16 |
|
|
17 | 15 |
from errors import OperationError, MissingCapabilityError |
18 |
from rpc import RPCError |
|
19 |
from retrieve import Get, GetConfig |
|
20 |
from edit import EditConfig, CopyConfig, DeleteConfig, Validate, Commit, DiscardChanges |
|
16 |
from rpc import RPC, RPCReply, RPCError
|
|
17 |
from retrieve import Get, GetConfig, GetReply
|
|
18 |
from edit import EditConfig, CopyConfig, DeleteConfig, Validate, Commit, DiscardChanges, ConfirmedCommit
|
|
21 | 19 |
from session import CloseSession, KillSession |
22 | 20 |
from lock import Lock, Unlock, LockContext |
23 |
from subscribe import CreateSubscription |
|
21 |
#from subscribe import CreateSubscription
|
|
24 | 22 |
|
25 | 23 |
OPERATIONS = { |
26 | 24 |
'get': Get, |
... | ... | |
38 | 36 |
} |
39 | 37 |
|
40 | 38 |
__all__ = [ |
39 |
'RPC', |
|
40 |
'RPCReply', |
|
41 | 41 |
'RPCError', |
42 | 42 |
'OPERATIONS', |
43 | 43 |
'Get', |
44 | 44 |
'GetConfig', |
45 |
'GetReply', |
|
45 | 46 |
'EditConfig', |
46 | 47 |
'CopyConfig', |
47 | 48 |
'Validate', |
48 | 49 |
'Commit', |
50 |
'ConfirmedCommit' |
|
49 | 51 |
'DiscardChanges', |
50 | 52 |
'DeleteConfig', |
51 | 53 |
'Lock', |
52 | 54 |
'Unlock', |
53 | 55 |
'LockContext', |
54 | 56 |
'CloseSession', |
55 |
'KillSession', |
|
56 |
'CreateSubscription', |
|
57 |
'KillSession' |
|
57 | 58 |
] |
b/ncclient/operations/edit.py | ||
---|---|---|
21 | 21 |
"Operations related to configuration editing" |
22 | 22 |
|
23 | 23 |
class EditConfig(RPC): |
24 |
|
|
25 |
# tested: no |
|
26 |
# combed: yes |
|
27 |
|
|
24 |
|
|
28 | 25 |
SPEC = {'tag': 'edit-config', 'subtree': []} |
29 |
|
|
26 |
|
|
30 | 27 |
def request(self, target=None, config=None, default_operation=None, |
31 | 28 |
test_option=None, error_option=None): |
32 | 29 |
util.one_of(target, config) |
... | ... | |
52 | 49 |
'tag': 'error-option', |
53 | 50 |
'text': error_option |
54 | 51 |
}) |
52 |
return self._request(spec) |
|
55 | 53 |
|
56 | 54 |
|
57 | 55 |
class DeleteConfig(RPC): |
58 |
|
|
59 |
# tested: no |
|
60 |
# combed: yes |
|
61 |
|
|
56 |
|
|
62 | 57 |
SPEC = {'tag': 'delete-config', 'subtree': []} |
63 |
|
|
58 |
|
|
64 | 59 |
def request(self, target): |
65 | 60 |
spec = DeleteConfig.SPEC.copy() |
66 |
spec['subtree'].append(util.store_or_url('source', source, self._assert))
|
|
61 |
spec['subtree'].append(util.store_or_url('target', target, self._assert))
|
|
67 | 62 |
return self._request(spec) |
68 | 63 |
|
69 | 64 |
|
70 | 65 |
class CopyConfig(RPC): |
71 |
|
|
72 |
# tested: no |
|
73 |
# combed: yes |
|
74 |
|
|
66 |
|
|
75 | 67 |
SPEC = {'tag': 'copy-config', 'subtree': []} |
76 |
|
|
68 |
|
|
77 | 69 |
def request(self, source, target): |
78 | 70 |
spec = CopyConfig.SPEC.copy() |
79 | 71 |
spec['subtree'].append(util.store_or_url('source', source, self._assert)) |
80 |
spec['subtree'].append(util.store_or_url('target', source, self._assert))
|
|
72 |
spec['subtree'].append(util.store_or_url('target', target, self._assert))
|
|
81 | 73 |
return self._request(spec) |
82 | 74 |
|
83 | 75 |
|
84 | 76 |
class Validate(RPC): |
85 |
|
|
86 |
# tested: no |
|
87 |
# combed: yes |
|
88 |
|
|
89 |
'config attr shd not include <config> root' |
|
90 |
|
|
77 |
|
|
91 | 78 |
DEPENDS = [':validate'] |
92 |
|
|
79 |
|
|
93 | 80 |
SPEC = {'tag': 'validate', 'subtree': []} |
94 |
|
|
95 |
def request(self, source=None, config=None):
|
|
96 |
util.one_of(source, config)
|
|
81 |
|
|
82 |
def request(self, source): |
|
83 |
# determine if source is a <config> element
|
|
97 | 84 |
spec = Validate.SPEC.copy() |
98 |
if config is None: |
|
85 |
try: |
|
86 |
spec['subtree'].append({ |
|
87 |
'tag': 'source', |
|
88 |
'subtree': content.validated_root(config, ('config', content.qualify('config'))) |
|
89 |
}) |
|
90 |
except ContentError: |
|
99 | 91 |
spec['subtree'].append(util.store_or_url('source', source, self._assert)) |
100 |
else: |
|
101 |
spec['subtree'].append(content.validated_root(config, 'config')) |
|
102 | 92 |
return self._request(spec) |
103 | 93 |
|
104 | 94 |
|
105 | 95 |
class Commit(RPC): |
106 |
|
|
107 |
# tested: no |
|
108 |
# combed: yes |
|
109 |
|
|
96 |
|
|
110 | 97 |
DEPENDS = [':candidate'] |
111 |
|
|
98 |
|
|
112 | 99 |
SPEC = {'tag': 'commit', 'subtree': []} |
113 |
|
|
100 |
|
|
114 | 101 |
def _parse_hook(self): |
115 | 102 |
pass |
116 |
|
|
117 |
def request(self, confirmed=False, timeout=None):
|
|
103 |
|
|
104 |
def request(self, confirmed=False): |
|
118 | 105 |
spec = SPEC.copy() |
119 | 106 |
if confirmed: |
120 | 107 |
self._assert(':confirmed-commit') |
... | ... | |
128 | 115 |
|
129 | 116 |
|
130 | 117 |
class DiscardChanges(RPC): |
131 |
|
|
132 |
# tested: no |
|
133 |
# combed: yes |
|
134 |
|
|
118 |
|
|
135 | 119 |
DEPENDS = [':candidate'] |
136 |
|
|
120 |
|
|
137 | 121 |
SPEC = {'tag': 'discard-changes'} |
138 | 122 |
|
139 | 123 |
|
140 | 124 |
class ConfirmedCommit(Commit): |
141 | 125 |
"psuedo-op" |
142 |
|
|
143 |
# tested: no |
|
144 |
# combed: yes |
|
145 |
|
|
126 |
|
|
146 | 127 |
DEPENDS = [':candidate', ':confirmed-commit'] |
147 |
|
|
148 |
def request(self, timeout=None):
|
|
149 |
"Commit changes; requireing that a confirming commit follow"
|
|
150 |
return Commit.request(self, confirmed=True, timeout=timeout)
|
|
151 |
|
|
128 |
|
|
129 |
def request(self): |
|
130 |
"Commit changes requiring that a confirm/discard follow"
|
|
131 |
return Commit.request(self, confirmed=True) |
|
132 |
|
|
152 | 133 |
def confirm(self): |
153 |
"Make the confirming commit"
|
|
134 |
"Confirm changes"
|
|
154 | 135 |
return Commit.request(self, confirmed=True) |
155 |
|
|
136 |
|
|
156 | 137 |
def discard(self): |
138 |
"Discard changes" |
|
157 | 139 |
return DiscardChanges(self.session, self.async, self.timeout).request() |
b/ncclient/operations/errors.py | ||
---|---|---|
19 | 19 |
|
20 | 20 |
class MissingCapabilityError(NCClientError): |
21 | 21 |
pass |
22 |
|
b/ncclient/operations/lock.py | ||
---|---|---|
17 | 17 |
from rpc import RPC |
18 | 18 |
|
19 | 19 |
class Lock(RPC): |
20 |
|
|
21 |
# tested: no |
|
22 |
# combed: yes |
|
23 |
|
|
20 |
|
|
24 | 21 |
SPEC = { |
25 | 22 |
'tag': 'lock', |
26 | 23 |
'subtree': { |
... | ... | |
28 | 25 |
'subtree': {'tag': None } |
29 | 26 |
} |
30 | 27 |
} |
31 |
|
|
32 |
def request(self, target): |
|
28 |
|
|
29 |
def request(self, target, *args, **kwds):
|
|
33 | 30 |
spec = Lock.SPEC.copy() |
34 | 31 |
spec['subtree']['subtree']['tag'] = target |
35 |
return self._request(spec) |
|
32 |
return self._request(spec, *args, **kwds)
|
|
36 | 33 |
|
37 | 34 |
|
38 | 35 |
class Unlock(RPC): |
39 |
|
|
40 |
# tested: no |
|
41 |
# combed: yes |
|
42 |
|
|
36 |
|
|
43 | 37 |
SPEC = { |
44 | 38 |
'tag': 'unlock', |
45 | 39 |
'subtree': { |
... | ... | |
47 | 41 |
'subtree': {'tag': None } |
48 | 42 |
} |
49 | 43 |
} |
50 |
|
|
51 |
def request(self, target): |
|
44 |
|
|
45 |
def request(self, target, *args, **kwds):
|
|
52 | 46 |
spec = Unlock.SPEC.copy() |
53 | 47 |
spec['subtree']['subtree']['tag'] = target |
54 |
return self._request(spec) |
|
48 |
return self._request(spec, *args, **kwds)
|
|
55 | 49 |
|
56 | 50 |
|
57 | 51 |
class LockContext: |
58 |
|
|
59 |
# tested: no |
|
60 |
# combed: yes |
|
61 |
|
|
52 |
|
|
62 | 53 |
def __init__(self, session, target): |
63 | 54 |
self.session = session |
64 | 55 |
self.target = target |
65 |
|
|
56 |
|
|
66 | 57 |
def __enter__(self): |
67 | 58 |
reply = Lock(self.session).request(self.target) |
68 | 59 |
if not reply.ok: |
69 | 60 |
raise reply.error |
70 | 61 |
else: |
71 | 62 |
return self |
72 |
|
|
63 |
|
|
73 | 64 |
def __exit__(self, *args): |
74 | 65 |
reply = Unlock(session).request(self.target) |
75 | 66 |
if not reply.ok: |
b/ncclient/operations/retrieve.py | ||
---|---|---|
19 | 19 |
import util |
20 | 20 |
|
21 | 21 |
class GetReply(RPCReply): |
22 |
|
|
23 |
'Adds data attribute' |
|
24 |
|
|
25 |
# tested: no |
|
26 |
# combed: yes |
|
27 |
|
|
22 |
|
|
23 |
"""Adds attributes for the *<data>* element to :class:`RPCReply`, pertinent |
|
24 |
to the *<get>* or *<get-config>* operations.""" |
|
25 |
|
|
28 | 26 |
def _parsing_hook(self, root): |
29 | 27 |
self._data = None |
30 | 28 |
if not self._errors: |
31 |
self._data = content.find(root, 'data', nslist=[content.BASE_NS, content.CISCO_BS]) |
|
32 |
|
|
29 |
self._data = content.find(root, 'data', |
|
30 |
nslist=[content.BASE_NS, |
|
31 |
content.CISCO_BS]) |
|
32 |
|
|
33 | 33 |
@property |
34 |
def data(self): |
|
34 |
def data_ele(self): |
|
35 |
"As an :class:`~xml.etree.ElementTree.Element`" |
|
35 | 36 |
if not self._parsed: |
36 | 37 |
self.parse() |
37 | 38 |
return self._data |
38 | 39 |
|
40 |
@property |
|
41 |
def data_xml(self): |
|
42 |
"As an XML string" |
|
43 |
if not self._parsed: |
|
44 |
self.parse() |
|
45 |
return content.ele2xml(self._data) |
|
46 |
|
|
47 |
data = data_ele |
|
48 |
|
|
49 |
|
|
39 | 50 |
class Get(RPC): |
40 |
|
|
41 |
# tested: no |
|
42 |
# combed: yes |
|
43 |
|
|
51 |
|
|
52 |
"*<get>* RPC" |
|
53 |
|
|
44 | 54 |
SPEC = { |
45 | 55 |
'tag': 'get', |
46 | 56 |
'subtree': [] |
47 | 57 |
} |
48 |
|
|
58 |
|
|
49 | 59 |
REPLY_CLS = GetReply |
50 |
|
|
60 |
|
|
51 | 61 |
def request(self, filter=None): |
52 | 62 |
spec = Get.SPEC.copy() |
53 | 63 |
if filter is not None: |
... | ... | |
57 | 67 |
|
58 | 68 |
class GetConfig(RPC): |
59 | 69 |
|
60 |
# tested: no |
|
61 |
# combed: yes |
|
62 |
|
|
70 |
"*<get-config>* RPC" |
|
71 |
|
|
63 | 72 |
SPEC = { |
64 | 73 |
'tag': 'get-config', |
65 | 74 |
'subtree': [] |
66 | 75 |
} |
67 |
|
|
76 |
|
|
68 | 77 |
REPLY_CLS = GetReply |
69 |
|
|
78 |
|
|
70 | 79 |
def request(self, source, filter=None): |
71 |
""" |
|
72 |
`filter` has to be a tuple of (type, criteria) |
|
73 |
The type may be one of 'xpath' or 'subtree' |
|
74 |
The criteria may be an ElementTree.Element, an XML fragment, or tree specification |
|
75 |
""" |
|
76 | 80 |
spec = GetConfig.SPEC.copy() |
77 | 81 |
spec['subtree'].append(util.store_or_url('source', source, self._assert)) |
78 | 82 |
if filter is not None: |
b/ncclient/operations/rpc.py | ||
---|---|---|
17 | 17 |
from weakref import WeakValueDictionary |
18 | 18 |
|
19 | 19 |
from ncclient import content |
20 |
from ncclient.capabilities import check |
|
20 | 21 |
from ncclient.transport import SessionListener |
21 | 22 |
|
22 | 23 |
from errors import OperationError |
... | ... | |
27 | 28 |
|
28 | 29 |
class RPCReply: |
29 | 30 |
|
31 |
"""Represents an *<rpc-reply>*. Only concerns itself with whether the |
|
32 |
operation was successful. Note that if the reply has not yet been parsed |
|
33 |
there is a one-time parsing overhead to accessing the :attr:`ok` and |
|
34 |
:attr:`error`/:attr:`errors` attributes.""" |
|
35 |
|
|
30 | 36 |
def __init__(self, raw): |
31 | 37 |
self._raw = raw |
32 | 38 |
self._parsed = False |
... | ... | |
36 | 42 |
def __repr__(self): |
37 | 43 |
return self._raw |
38 | 44 |
|
39 |
def _parsing_hook(self, root): pass |
|
45 |
def _parsing_hook(self, root): |
|
46 |
"""Subclass can implement. |
|
47 |
|
|
48 |
:type root: :class:`~xml.etree.ElementTree.Element` |
|
49 |
""" |
|
50 |
pass |
|
40 | 51 |
|
41 | 52 |
def parse(self): |
53 |
"""Parse the *<rpc-reply>*""" |
|
42 | 54 |
if self._parsed: |
43 | 55 |
return |
44 | 56 |
root = self._root = content.xml2ele(self._raw) # <rpc-reply> element |
... | ... | |
65 | 77 |
|
66 | 78 |
@property |
67 | 79 |
def xml(self): |
68 |
'<rpc-reply> as returned'
|
|
80 |
"*<rpc-reply>* as returned"
|
|
69 | 81 |
return self._raw |
70 | 82 |
|
71 | 83 |
@property |
72 | 84 |
def ok(self): |
85 |
"Boolean value indicating if there were no errors." |
|
73 | 86 |
if not self._parsed: |
74 | 87 |
self.parse() |
75 | 88 |
return not self._errors # empty list => false |
76 | 89 |
|
77 | 90 |
@property |
78 | 91 |
def error(self): |
92 |
"Short for :attr:`errors`[0], returning :const:`None` if there were no errors." |
|
79 | 93 |
if not self._parsed: |
80 | 94 |
self.parse() |
81 | 95 |
if self._errors: |
... | ... | |
85 | 99 |
|
86 | 100 |
@property |
87 | 101 |
def errors(self): |
88 |
'List of RPCError objects. Will be empty if no <rpc-error> elements in reply.'
|
|
102 |
"List of :class:`RPCError` objects. Will be empty if there were no :class:`<rpc-error>` elements in reply."
|
|
89 | 103 |
if not self._parsed: |
90 | 104 |
self.parse() |
91 | 105 |
return self._errors |
... | ... | |
93 | 107 |
|
94 | 108 |
class RPCError(OperationError): # raise it if you like |
95 | 109 |
|
110 |
"""Represents an *<rpc-error>*. It is an instance of :exc:`OperationError` |
|
111 |
so it can be raised like any other exception.""" |
|
112 |
|
|
96 | 113 |
def __init__(self, err_dict): |
97 | 114 |
self._dict = err_dict |
98 | 115 |
if self.message is not None: |
... | ... | |
102 | 119 |
|
103 | 120 |
@property |
104 | 121 |
def type(self): |
122 |
"`string` represeting *error-type* element" |
|
105 | 123 |
return self.get('error-type', None) |
106 | 124 |
|
107 | 125 |
@property |
108 | 126 |
def severity(self): |
127 |
"`string` represeting *error-severity* element" |
|
109 | 128 |
return self.get('error-severity', None) |
110 | 129 |
|
111 | 130 |
@property |
112 | 131 |
def tag(self): |
132 |
"`string` represeting *error-tag* element" |
|
113 | 133 |
return self.get('error-tag', None) |
114 | 134 |
|
115 | 135 |
@property |
116 | 136 |
def path(self): |
137 |
"`string` or :const:`None`; represeting *error-path* element" |
|
117 | 138 |
return self.get('error-path', None) |
118 | 139 |
|
119 | 140 |
@property |
120 | 141 |
def message(self): |
142 |
"`string` or :const:`None`; represeting *error-message* element" |
|
121 | 143 |
return self.get('error-message', None) |
122 | 144 |
|
123 | 145 |
@property |
124 | 146 |
def info(self): |
147 |
"`string` or :const:`None`, represeting *error-info* element" |
|
125 | 148 |
return self.get('error-info', None) |
126 | 149 |
|
127 | 150 |
## dictionary interface |
... | ... | |
151 | 174 |
|
152 | 175 |
class RPCReplyListener(SessionListener): |
153 | 176 |
|
177 |
# internal use |
|
178 |
|
|
154 | 179 |
# one instance per session |
155 | 180 |
def __new__(cls, session): |
156 | 181 |
instance = session.get_listener_instance(cls) |
... | ... | |
191 | 216 |
else: |
192 | 217 |
logger.warning('<rpc-reply> without message-id received: %s' % raw) |
193 | 218 |
logger.debug('delivering to %r' % rpc) |
194 |
rpc.deliver(raw) |
|
219 |
rpc.deliver_reply(raw)
|
|
195 | 220 |
|
196 | 221 |
def errback(self, err): |
197 | 222 |
for rpc in self._id2rpc.values(): |
198 |
rpc.error(err) |
|
223 |
rpc.deliver_error(err)
|
|
199 | 224 |
|
200 | 225 |
|
201 | 226 |
class RPC(object): |
202 | 227 |
|
228 |
"Directly corresponds to *<rpc>* requests. Handles making the request, and taking delivery of the reply." |
|
229 |
|
|
230 |
# : Subclasses can specify their dependencies on capabilities. List of URI's |
|
231 |
# or abbreviated names, e.g. ':writable-running'. These are verified at the |
|
232 |
# time of object creation. If the capability is not available, a |
|
233 |
# :exc:`MissingCapabilityError` is raised. |
|
203 | 234 |
DEPENDS = [] |
235 |
|
|
236 |
# : Subclasses can specify a different reply class, but it must be a |
|
237 |
# subclass of :class:`RPCReply`. |
|
204 | 238 |
REPLY_CLS = RPCReply |
205 | 239 |
|
206 | 240 |
def __init__(self, session, async=False, timeout=None): |
207 |
if not session.can_pipeline: |
|
208 |
raise UserWarning('Asynchronous mode not supported for this device/session') |
|
209 | 241 |
self._session = session |
210 | 242 |
try: |
211 | 243 |
for cap in self.DEPENDS: |
... | ... | |
221 | 253 |
self._listener.register(self._id, self) |
222 | 254 |
self._reply = None |
223 | 255 |
self._error = None |
224 |
self._reply_event = Event()
|
|
256 |
self._event = Event() |
|
225 | 257 |
|
226 | 258 |
def _build(self, opspec): |
227 |
"TODO: docstring"
|
|
259 |
# internal
|
|
228 | 260 |
spec = { |
229 | 261 |
'tag': content.qualify('rpc'), |
230 | 262 |
'attrib': {'message-id': self._id}, |
231 |
'subtree': opspec
|
|
263 |
'subtree': [ opspec ]
|
|
232 | 264 |
} |
233 | 265 |
return content.dtree2xml(spec) |
234 | 266 |
|
235 | 267 |
def _request(self, op): |
268 |
"""Subclasses call this method to make the RPC request. |
|
269 |
|
|
270 |
In asynchronous mode, returns an :class:`~threading.Event` which is set |
|
271 |
when the reply has been received or an error occured. It is prudent, |
|
272 |
therefore, to check the :attr:`error` attribute before accesing |
|
273 |
:attr:`reply`. |
|
274 |
|
|
275 |
Otherwise, waits until the reply is received and returns |
|
276 |
:class:`RPCReply`. |
|
277 |
|
|
278 |
:arg opspec: :ref:`dtree` for the operation |
|
279 |
:type opspec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element` |
|
280 |
:rtype: :class:`~threading.Event` or :class:`RPCReply` |
|
281 |
""" |
|
236 | 282 |
req = self._build(op) |
237 | 283 |
self._session.send(req) |
238 | 284 |
if self._async: |
239 |
return self._reply_event
|
|
285 |
return self._event |
|
240 | 286 |
else: |
241 |
self._reply_event.wait(self._timeout)
|
|
242 |
if self._reply_event.isSet():
|
|
287 |
self._event.wait(self._timeout) |
|
288 |
if self._event.isSet(): |
|
243 | 289 |
if self._error: |
244 | 290 |
raise self._error |
245 | 291 |
self._reply.parse() |
... | ... | |
247 | 293 |
else: |
248 | 294 |
raise ReplyTimeoutError |
249 | 295 |
|
250 |
def request(self): |
|
251 |
return self._request(self.SPEC) |
|
296 |
def request(self, *args, **kwds): |
|
297 |
"Subclasses implement this method. Here, the operation is to be constructed as a :ref:`dtree`, and the result of :meth:`_request` returned." |
|
298 |
return self._request(self.SPEC, *args, **kwds) |
|
252 | 299 |
|
253 | 300 |
def _delivery_hook(self): |
254 |
'For subclasses' |
|
301 |
"""Subclasses can implement this method. Will be called after |
|
302 |
initialising the :attr:`reply` or :attr:`error` attribute and before |
|
303 |
setting the :attr:`event`""" |
|
255 | 304 |
pass |
256 | 305 |
|
257 | 306 |
def _assert(self, capability): |
307 |
"""Subclasses can use this method to verify that a capability is available |
|
308 |
with the NETCONF server, before making a request that requires it. A |
|
309 |
:class:`MissingCapabilityError` will be raised if the capability is not |
|
310 |
available.""" |
|
258 | 311 |
if capability not in self._session.server_capabilities: |
259 | 312 |
raise MissingCapabilityError('Server does not support [%s]' % cap) |
260 | 313 |
|
261 |
def deliver(self, raw): |
|
314 |
def deliver_reply(self, raw): |
|
315 |
# internal use |
|
262 | 316 |
self._reply = self.REPLY_CLS(raw) |
263 | 317 |
self._delivery_hook() |
264 |
self._reply_event.set()
|
|
318 |
self._event.set() |
|
265 | 319 |
|
266 |
def error(self, err): |
|
320 |
def deliver_error(self, err): |
|
321 |
# internal use |
|
267 | 322 |
self._error = err |
268 |
self._reply_event.set() |
|
269 |
|
|
270 |
@property |
|
271 |
def has_reply(self): |
|
272 |
return self._reply_event.is_set() |
|
323 |
self._delivery_hook() |
|
324 |
self._event.set() |
|
273 | 325 |
|
274 | 326 |
@property |
275 | 327 |
def reply(self): |
276 |
if self.error: |
|
277 |
raise self._error |
|
328 |
":class:`RPCReply` element if reply has been received or :const:`None`" |
|
278 | 329 |
return self._reply |
279 | 330 |
|
280 | 331 |
@property |
332 |
def error(self): |
|
333 |
""":exc:`Exception` type if an error occured or :const:`None`. |
|
334 |
|
|
335 |
This attribute should be checked if the request was made asynchronously, |
|
336 |
so that it can be determined if :attr:`event` being set is because of a |
|
337 |
reply or error. |
|
338 |
|
|
339 |
.. note:: |
|
340 |
This represents an error which prevented a reply from being |
|
341 |
received. An *<rpc-error>* does not fall in that category -- see |
|
342 |
:class:`RPCReply` for that. |
|
343 |
""" |
|
344 |
return self._error |
|
345 |
|
|
346 |
@property |
|
281 | 347 |
def id(self): |
348 |
"The *message-id* for this RPC" |
|
282 | 349 |
return self._id |
283 | 350 |
|
284 | 351 |
@property |
285 | 352 |
def session(self): |
353 |
"""The :class:`~ncclient.transport.Session` object associated with this |
|
354 |
RPC""" |
|
286 | 355 |
return self._session |
287 | 356 |
|
288 | 357 |
@property |
289 |
def reply_event(self): |
|
290 |
return self._reply_event |
|
358 |
def event(self): |
|
359 |
""":class:`~threading.Event` that is set when reply has been received or |
|
360 |
error occured.""" |
|
361 |
return self._event |
|
362 |
|
|
363 |
def set_async(self, async=True): |
|
364 |
"""Set asynchronous mode for this RPC.""" |
|
365 |
self._async = async |
|
366 |
if async and not session.can_pipeline: |
|
367 |
raise UserWarning('Asynchronous mode not supported for this device/session') |
|
368 |
|
|
369 |
def set_timeout(self, timeout): |
|
370 |
"""Set the timeout for synchronous waiting defining how long the RPC |
|
371 |
request will block on a reply before raising an error.""" |
|
372 |
self._timeout = timeout |
|
291 | 373 |
|
292 |
def set_async(self, bool): self._async = bool
|
|
374 |
#: Whether this RPC is asynchronous
|
|
293 | 375 |
async = property(fget=lambda self: self._async, fset=set_async) |
294 | 376 |
|
295 |
def set_timeout(self, timeout): self._timeout = timeout
|
|
377 |
#: Timeout for synchronous waiting
|
|
296 | 378 |
timeout = property(fget=lambda self: self._timeout, fset=set_timeout) |
b/ncclient/operations/session.py | ||
---|---|---|
17 | 17 |
from rpc import RPC |
18 | 18 |
|
19 | 19 |
class CloseSession(RPC): |
20 |
# tested: no |
|
21 |
# combed: yes
|
|
22 |
|
|
20 |
|
|
21 |
"*<close-session>* RPC. The connection to NETCONF server is also closed."
|
|
22 |
|
|
23 | 23 |
SPEC = { 'tag': 'close-session' } |
24 |
|
|
25 |
def _delivery_hook(self): |
|
24 |
|
|
25 |
def _delivsery_hook(self):
|
|
26 | 26 |
self.session.close() |
27 | 27 |
|
28 | 28 |
|
29 | 29 |
class KillSession(RPC): |
30 |
# tested: no |
|
31 |
# combed: yes
|
|
32 |
|
|
30 |
|
|
31 |
"*<kill-session>* RPC."
|
|
32 |
|
|
33 | 33 |
SPEC = { |
34 | 34 |
'tag': 'kill-session', |
35 | 35 |
'subtree': [] |
36 | 36 |
} |
37 |
|
|
38 |
def request(self, session_id): |
|
37 |
|
|
38 |
def request(self, session_id, *args, **kwds):
|
|
39 | 39 |
spec = KillSession.SPEC.copy() |
40 | 40 |
if not isinstance(session_id, basestring): # just making sure... |
41 | 41 |
session_id = str(session_id) |
... | ... | |
43 | 43 |
'tag': 'session-id', |
44 | 44 |
'text': session_id |
45 | 45 |
}) |
46 |
return self._request(spec) |
|
46 |
return self._request(spec, *args, **kwds) |
b/ncclient/operations/util.py | ||
---|---|---|
50 | 50 |
'subtree': criteria |
51 | 51 |
} |
52 | 52 |
else: |
53 |
rep = content.validated_element(spec, 'filter', 'type') |
|
53 |
rep = content.validated_element(spec, ['filter', content.qualify('filter')], |
|
54 |
attrs=[('type', content.qualify('type'))]) |
|
54 | 55 |
try: |
55 | 56 |
type = rep['type'] |
56 | 57 |
except KeyError: |
b/ncclient/transport/ssh.py | ||
---|---|---|
278 | 278 |
|
279 | 279 |
if saved_exception is not None: |
280 | 280 |
# need pep-3134 to do this right |
281 |
raise SSHAuthenticationError(repr(saved_exception))
|
|
281 |
raise AuthenticationError(repr(saved_exception)) |
|
282 | 282 |
|
283 |
raise SSHAuthenticationError('No authentication methods available')
|
|
283 |
raise AuthenticationError('No authentication methods available') |
|
284 | 284 |
|
285 | 285 |
def run(self): |
286 | 286 |
chan = self._channel |
Also available in: Unified diff