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):
111 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())
137 'tag': self.severity,
139 'message': self.message,
145 "`string` representing text of *error-type* element"
150 "`string` representing text of *error-severity* element"
151 return self._severity
155 "`string` representing text of *error-tag* element"
160 "`string` or :const:`None`; representing text of *error-path* element"
165 "`string` or :const:`None`; representing text of *error-message* element"
170 "`string` (XML) or :const:`None`, representing *error-info* element"
174 class RPCReplyListener(SessionListener):
178 # one instance per session -- maybe there is a better way??
179 def __new__(cls, session):
180 instance = session.get_listener_instance(cls)
182 instance = object.__new__(cls)
183 instance._lock = Lock()
184 instance._id2rpc = {}
185 #instance._pipelined = session.can_pipeline
186 session.add_listener(instance)
189 def register(self, id, rpc):
191 self._id2rpc[id] = rpc
193 def callback(self, root, raw):
195 if tag != qualify("rpc-reply"):
197 for key in attrs: # in the <rpc-reply> attributes
198 if key == "message-id": # if we found msgid attr
199 id = attrs[key] # get the msgid
202 rpc = self._id2rpc[id] # the corresponding rpc
203 logger.debug("Delivering to %r" % rpc)
204 rpc.deliver_reply(raw)
206 raise OperationError("Unknown message-id: %s", id)
207 # no catching other exceptions, fail loudly if must
209 # if no error delivering, can del the reference to the RPC
213 raise OperationError("Could not find 'message-id' attribute in <rpc-reply>")
215 def errback(self, err):
217 for rpc in self._id2rpc.values():
218 rpc.deliver_error(err)
225 """Base class for all operations.
227 Directly corresponds to *<rpc>* requests. Handles making the request, and
228 taking delivery of the reply.
231 #: Subclasses can specify their dependencies on capabilities. List of URI's
232 # or abbreviated names, e.g. ':writable-running'. These are verified at the
233 # time of object creation. If the capability is not available, a
234 # :exc:`MissingCapabilityError` is raised.
237 #: Subclasses can specify a different reply class, but it must be a
238 # subclass of :class:`RPCReply`.
241 def __init__(self, session, async=False, timeout=None, raise_mode="none"):
242 self._session = session
244 for cap in self.DEPENDS:
246 except AttributeError:
249 self._timeout = timeout
250 self._raise_mode = raise_mode
251 self._id = uuid1().urn # Keeps things simple instead of having a class attr that has to be locked
252 self._listener = RPCReplyListener(session)
253 self._listener.register(self._id, self)
256 self._event = Event()
258 def _build(self, subele):
260 ele = new_ele("rpc", {"message-id": self._id}, xmlns=BASE_NS_1_0)
264 def _request(self, op):
265 """Subclasses call this method to make the RPC request.
267 In synchronous mode, waits until the reply is received and returns
270 In asynchronous mode, returns immediately, returning a reference to this
271 object. The :attr:`event` attribute will be set when the reply has been
272 received (see :attr:`reply`) or an error occured (see :attr:`error`).
274 :type opspec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
275 :rtype: :class:`RPCReply` (sync) or :class:`RPC` (async)
277 logger.info('Requesting %r' % self.__class__.__name__)
278 req = self._build(op)
279 self._session.send(req)
281 logger.debug('Async request, returning %r', self)
284 logger.debug('Sync request, will wait for timeout=%r' %
286 self._event.wait(self._timeout)
287 if self._event.isSet():
289 # Error that prevented reply delivery
292 if self._reply.error is not None:
293 # <rpc-error>'s [ RPCError ]
294 if self._raise_mode == "all":
295 raise self._reply.error
296 elif (self._raise_mode == "errors" and
297 self._reply.error.type == "error"):
298 raise self._reply.error
301 raise TimeoutExpiredError
303 def request(self, *args, **kwds):
304 "Subclasses implement this method."
305 return self._request(self.SPEC)
307 def _assert(self, capability):
308 """Subclasses can use this method to verify that a capability is available
309 with the NETCONF server, before making a request that requires it. A
310 :exc:`MissingCapabilityError` will be raised if the capability is not
312 if capability not in self._session.server_capabilities:
313 raise MissingCapabilityError('Server does not support [%s]' %
316 def deliver_reply(self, raw):
318 self._reply = self.REPLY_CLS(raw)
321 def deliver_error(self, err):
328 ":class:`RPCReply` element if reply has been received or :const:`None`"
333 """:exc:`Exception` type if an error occured or :const:`None`.
336 This represents an error which prevented a reply from being
337 received. An *<rpc-error>* does not fall in that category -- see
338 :class:`RPCReply` for that.
344 "The *message-id* for this RPC"
349 """The :class:`~ncclient.transport.Session` object associated with this
355 """:class:`~threading.Event` that is set when reply has been received or
359 def set_async(self, async=True):
360 """Set asynchronous mode for this RPC."""
362 if async and not session.can_pipeline:
363 raise UserWarning('Asynchronous mode not supported for this device/session')
365 def set_raise_mode(self, mode):
366 assert(choice in ("all", "errors", "none"))
367 self._raise_mode = mode
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. Irrelevant for
372 asynchronous usage."""
373 self._timeout = timeout
375 #: Whether this RPC is asynchronous
376 is_async = property(fget=lambda self: self._async, fset=set_async)
378 #: Timeout for synchronous waiting
379 timeout = property(fget=lambda self: self._timeout, fset=set_timeout)