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-tag"):
118 self._tag = subele.text
119 elif subele.tag == qualify("error-severity"):
120 self._severity = subele.text
121 elif subele.tag == qualify("error-info"):
122 self._info = subele.text
123 elif subele.tag == qualify("error-path"):
124 self._path = subele.text
125 elif subele.tag == qualify("error-message"):
126 self._message = subele.text
127 if self.message is not None:
128 OperationError.__init__(self, self.message)
130 OperationError.__init__(self)
134 "`string` representing text of *error-type* element"
139 "`string` representing text of *error-severity* element"
140 return self._severity
144 "`string` representing text of *error-tag* element"
149 "`string` or :const:`None`; representing text of *error-path* element"
154 "`string` or :const:`None`; representing text of *error-message* element"
159 "`string` (XML) or :const:`None`, representing *error-info* element"
163 class RPCReplyListener(SessionListener):
167 # one instance per session -- maybe there is a better way??
168 def __new__(cls, session):
169 instance = session.get_listener_instance(cls)
171 instance = object.__new__(cls)
172 instance._lock = Lock()
173 instance._id2rpc = {}
174 #instance._pipelined = session.can_pipeline
175 session.add_listener(instance)
178 def register(self, id, rpc):
180 self._id2rpc[id] = rpc
182 def callback(self, root, raw):
184 if tag != qualify("rpc-reply"):
186 for key in attrs: # in the <rpc-reply> attributes
187 if key == "message-id": # if we found msgid attr
188 id = attrs[key] # get the msgid
191 rpc = self._id2rpc[id] # the corresponding rpc
192 logger.debug("Delivering to %r" % rpc)
193 rpc.deliver_reply(raw)
195 raise OperationError("Unknown message-id: %s", id)
196 # no catching other exceptions, fail loudly if must
198 # if no error delivering, can del the reference to the RPC
202 raise OperationError("Could not find 'message-id' attribute in <rpc-reply>")
204 def errback(self, err):
206 for rpc in self._id2rpc.values():
207 rpc.deliver_error(err)
214 """Base class for all operations.
216 Directly corresponds to *<rpc>* requests. Handles making the request, and
217 taking delivery of the reply.
220 #: Subclasses can specify their dependencies on capabilities. List of URI's
221 # or abbreviated names, e.g. ':writable-running'. These are verified at the
222 # time of object creation. If the capability is not available, a
223 # :exc:`MissingCapabilityError` is raised.
226 #: Subclasses can specify a different reply class, but it must be a
227 # subclass of :class:`RPCReply`.
230 def __init__(self, session, async=False, timeout=None, raise_mode="none"):
231 self._session = session
233 for cap in self.DEPENDS:
235 except AttributeError:
238 self._timeout = timeout
239 self._raise_mode = raise_mode
240 self._id = uuid1().urn # Keeps things simple instead of having a class attr that has to be locked
241 self._listener = RPCReplyListener(session)
242 self._listener.register(self._id, self)
245 self._event = Event()
247 def _build(self, subele):
249 ele = new_ele("rpc", {"message-id": self._id}, xmlns=BASE_NS_1_0)
253 def _request(self, op):
254 """Subclasses call this method to make the RPC request.
256 In synchronous mode, waits until the reply is received and returns
259 In asynchronous mode, returns immediately, returning a reference to this
260 object. The :attr:`event` attribute will be set when the reply has been
261 received (see :attr:`reply`) or an error occured (see :attr:`error`).
263 :type opspec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
264 :rtype: :class:`RPCReply` (sync) or :class:`RPC` (async)
266 logger.info('Requesting %r' % self.__class__.__name__)
267 req = self._build(op)
268 self._session.send(req)
270 logger.debug('Async request, returning %r', self)
273 logger.debug('Sync request, will wait for timeout=%r' %
275 self._event.wait(self._timeout)
276 if self._event.isSet():
278 # Error that prevented reply delivery
281 if self._reply.error is not None:
282 # <rpc-error>'s [ RPCError ]
283 if self._raise_mode == "all":
284 raise self._reply.error
285 elif (self._raise_mode == "errors" and
286 self._reply.error.type == "error"):
287 raise self._reply.error
290 raise TimeoutExpiredError
292 def request(self, *args, **kwds):
293 "Subclasses implement this method."
294 return self._request(self.SPEC)
296 def _assert(self, capability):
297 """Subclasses can use this method to verify that a capability is available
298 with the NETCONF server, before making a request that requires it. A
299 :exc:`MissingCapabilityError` will be raised if the capability is not
301 if capability not in self._session.server_capabilities:
302 raise MissingCapabilityError('Server does not support [%s]' %
305 def deliver_reply(self, raw):
307 self._reply = self.REPLY_CLS(raw)
310 def deliver_error(self, err):
317 ":class:`RPCReply` element if reply has been received or :const:`None`"
322 """:exc:`Exception` type if an error occured or :const:`None`.
325 This represents an error which prevented a reply from being
326 received. An *<rpc-error>* does not fall in that category -- see
327 :class:`RPCReply` for that.
333 "The *message-id* for this RPC"
338 """The :class:`~ncclient.transport.Session` object associated with this
344 """:class:`~threading.Event` that is set when reply has been received or
348 def set_async(self, async=True):
349 """Set asynchronous mode for this RPC."""
351 if async and not session.can_pipeline:
352 raise UserWarning('Asynchronous mode not supported for this device/session')
354 def set_raise_mode(self, mode):
355 assert(choice in ("all", "errors", "none"))
356 self._raise_mode = mode
358 def set_timeout(self, timeout):
359 """Set the timeout for synchronous waiting; defining how long the RPC
360 request will block on a reply before raising an error. Irrelevant for
361 asynchronous usage."""
362 self._timeout = timeout
364 #: Whether this RPC is asynchronous
365 is_async = property(fget=lambda self: self._async, fset=set_async)
367 #: Timeout for synchronous waiting
368 timeout = property(fget=lambda self: self._timeout, fset=set_timeout)