1 # Copyright 2009 Shikhar Bhushan
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 from threading import Event, Lock
16 from uuid import uuid1
18 from ncclient.xml_ import *
19 from ncclient.transport import SessionListener
21 from errors import OperationError, TimeoutExpiredError, MissingCapabilityError
24 logger = logging.getLogger("ncclient.operations.rpc")
29 """Represents an *<rpc-reply>*. Only concerns itself with whether the
30 operation was successful.
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
38 def __init__(self, raw):
47 def _parsing_hook(self, root):
48 """Subclass can implement.
50 :type root: :class:`~xml.etree.ElementTree.Element`
55 """Parse the *<rpc-reply>*"""
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"))
62 # Create RPCError objects from <rpc-error> elements
63 error = root.find(qualify("rpc-error"))
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)
73 "*<rpc-reply>* as returned"
78 "Boolean value indicating if there were no errors."
81 return not self._errors # empty list => false
85 """Short for :attr:`errors` [0]; :const:`None` if there were no errors.
90 return self._errors[0]
96 """`list` of :class:`RPCError` objects. Will be empty if there were no
97 *<rpc-error>* elements in reply.
104 class RPCError(OperationError):
106 """Represents an *<rpc-error>*. It is a type of :exc:`OperationError`
107 and can be raised like any other exception."""
109 def __init__(self, err):
112 self._severity = None
117 if subele.tag == qualify("error-type"):
118 self._type = subele.text
119 elif subele.tag == qualify("error-tag"):
120 self._tag = subele.text
121 elif subele.tag == qualify("error-severity"):
122 self._severity = subele.text
123 elif subele.tag == qualify("error-info"):
124 self._info = subele.text
125 elif subele.tag == qualify("error-path"):
126 self._path = subele.text
127 elif subele.tag == qualify("error-message"):
128 self._message = subele.text
129 if self.message is not None:
130 OperationError.__init__(self, self.message)
132 OperationError.__init__(self, self.to_dict())
138 'severity': self.severity,
140 'message': self.message,
146 "`string` representing text of *error-type* element"
151 "`string` representing text of *error-tag* element"
156 "`string` representing text of *error-severity* element"
157 return self._severity
161 "`string` or :const:`None`; representing text of *error-path* element"
166 "`string` or :const:`None`; representing text of *error-message* element"
171 "`string` (XML) or :const:`None`, representing *error-info* element"
175 class RPCReplyListener(SessionListener):
179 # one instance per session -- maybe there is a better way??
180 def __new__(cls, session):
181 instance = session.get_listener_instance(cls)
183 instance = object.__new__(cls)
184 instance._lock = Lock()
185 instance._id2rpc = {}
186 #instance._pipelined = session.can_pipeline
187 session.add_listener(instance)
190 def register(self, id, rpc):
192 self._id2rpc[id] = rpc
194 def callback(self, root, raw):
196 if tag != qualify("rpc-reply"):
198 for key in attrs: # in the <rpc-reply> attributes
199 if key == "message-id": # if we found msgid attr
200 id = attrs[key] # get the msgid
203 rpc = self._id2rpc[id] # the corresponding rpc
204 logger.debug("Delivering to %r" % rpc)
205 rpc.deliver_reply(raw)
207 raise OperationError("Unknown message-id: %s", id)
208 # no catching other exceptions, fail loudly if must
210 # if no error delivering, can del the reference to the RPC
214 raise OperationError("Could not find 'message-id' attribute in <rpc-reply>")
216 def errback(self, err):
218 for rpc in self._id2rpc.values():
219 rpc.deliver_error(err)
226 """Base class for all operations.
228 Directly corresponds to *<rpc>* requests. Handles making the request, and
229 taking delivery of the reply.
232 #: Subclasses can specify their dependencies on capabilities. List of URI's
233 # or abbreviated names, e.g. ':writable-running'. These are verified at the
234 # time of object creation. If the capability is not available, a
235 # :exc:`MissingCapabilityError` is raised.
238 #: Subclasses can specify a different reply class, but it must be a
239 # subclass of :class:`RPCReply`.
242 def __init__(self, session, async=False, timeout=None, raise_mode="none"):
243 self._session = session
245 for cap in self.DEPENDS:
247 except AttributeError:
250 self._timeout = timeout
251 self._raise_mode = raise_mode
252 self._id = uuid1().urn # Keeps things simple instead of having a class attr that has to be locked
253 self._listener = RPCReplyListener(session)
254 self._listener.register(self._id, self)
257 self._event = Event()
259 def _build(self, subele):
261 ele = new_ele("rpc", {"message-id": self._id}, xmlns=BASE_NS_1_0)
265 def _request(self, op):
266 """Subclasses call this method to make the RPC request.
268 In synchronous mode, waits until the reply is received and returns
271 In asynchronous mode, returns immediately, returning a reference to this
272 object. The :attr:`event` attribute will be set when the reply has been
273 received (see :attr:`reply`) or an error occured (see :attr:`error`).
275 :type opspec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
276 :rtype: :class:`RPCReply` (sync) or :class:`RPC` (async)
278 logger.info('Requesting %r' % self.__class__.__name__)
279 req = self._build(op)
280 self._session.send(req)
282 logger.debug('Async request, returning %r', self)
285 logger.debug('Sync request, will wait for timeout=%r' %
287 self._event.wait(self._timeout)
288 if self._event.isSet():
290 # Error that prevented reply delivery
293 if self._reply.error is not None:
294 # <rpc-error>'s [ RPCError ]
295 if self._raise_mode == "all":
296 raise self._reply.error
297 elif (self._raise_mode == "errors" and
298 self._reply.error.type == "error"):
299 raise self._reply.error
302 raise TimeoutExpiredError
304 def request(self, *args, **kwds):
305 "Subclasses implement this method."
306 return self._request(self.SPEC)
308 def _assert(self, capability):
309 """Subclasses can use this method to verify that a capability is available
310 with the NETCONF server, before making a request that requires it. A
311 :exc:`MissingCapabilityError` will be raised if the capability is not
313 if capability not in self._session.server_capabilities:
314 raise MissingCapabilityError('Server does not support [%s]' %
317 def deliver_reply(self, raw):
319 self._reply = self.REPLY_CLS(raw)
322 def deliver_error(self, err):
329 ":class:`RPCReply` element if reply has been received or :const:`None`"
334 """:exc:`Exception` type if an error occured or :const:`None`.
337 This represents an error which prevented a reply from being
338 received. An *<rpc-error>* does not fall in that category -- see
339 :class:`RPCReply` for that.
345 "The *message-id* for this RPC"
350 """The :class:`~ncclient.transport.Session` object associated with this
356 """:class:`~threading.Event` that is set when reply has been received or
360 def set_async(self, async=True):
361 """Set asynchronous mode for this RPC."""
363 if async and not session.can_pipeline:
364 raise UserWarning('Asynchronous mode not supported for this device/session')
366 def set_raise_mode(self, mode):
367 assert(choice in ("all", "errors", "none"))
368 self._raise_mode = mode
370 def set_timeout(self, timeout):
371 """Set the timeout for synchronous waiting; defining how long the RPC
372 request will block on a reply before raising an error. Irrelevant for
373 asynchronous usage."""
374 self._timeout = timeout
376 #: Whether this RPC is asynchronous
377 is_async = property(fget=lambda self: self._async, fset=set_async)
379 #: Timeout for synchronous waiting
380 timeout = property(fget=lambda self: self._timeout, fset=set_timeout)