Revision 4bc8021f ncclient/operations/rpc.py
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 |
""" |
Also available in: Unified diff