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."""
110 qualify("error-type"): "_type",
111 qualify("error-tag"): "_tag",
112 qualify("error-severity"): "_severity",
113 qualify("error-info"): "_info",
114 qualify("error-path"): "_path",
115 qualify("error-message"): "_message"
118 def __init__(self, err):
119 for attr in RPCError.tag_to_attr.values():
120 setattr(self, attr, None)
122 attr = RPCError.tag_to_attr.get(subele.tag, None)
124 setattr(self, attr, subele.text)
125 if self.message is not None:
126 OperationError.__init__(self, self.message)
128 OperationError.__init__(self, self.to_dict())
131 return dict([ (attr[1:], gettattr(self, attr)) for attr in RPCError.tag_to_attr.values() ])
135 "`string` representing text of *error-type* element"
140 "`string` representing text of *error-tag* element"
145 "`string` representing text of *error-severity* element"
146 return self._severity
150 "`string` or :const:`None`; representing text of *error-path* element"
155 "`string` or :const:`None`; representing text of *error-message* element"
160 "`string` (XML) or :const:`None`, representing *error-info* element"
164 class RPCReplyListener(SessionListener): # internal use
166 creation_lock = Lock()
168 # one instance per session -- maybe there is a better way??
169 def __new__(cls, session):
170 with RPCReplyListener.creation_lock:
171 instance = session.get_listener_instance(cls)
173 instance = object.__new__(cls)
174 instance._lock = Lock()
175 instance._id2rpc = {}
176 #instance._pipelined = session.can_pipeline
177 session.add_listener(instance)
180 def register(self, id, rpc):
182 self._id2rpc[id] = rpc
184 def callback(self, root, raw):
186 if tag != qualify("rpc-reply"):
188 for key in attrs: # in the <rpc-reply> attributes
189 if key == "message-id": # if we found msgid attr
190 id = attrs[key] # get the msgid
193 rpc = self._id2rpc[id] # the corresponding rpc
194 logger.debug("Delivering to %r" % rpc)
195 rpc.deliver_reply(raw)
197 raise OperationError("Unknown 'message-id': %s", id)
198 # no catching other exceptions, fail loudly if must
200 # if no error delivering, can del the reference to the RPC
204 raise OperationError("Could not find 'message-id' attribute in <rpc-reply>")
206 def errback(self, err):
208 for rpc in self._id2rpc.values():
209 rpc.deliver_error(err)
216 """Base class for all operations.
218 Directly corresponds to *<rpc>* requests. Handles making the request, and
219 taking delivery of the reply.
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.
228 #: Subclasses can specify a different reply class, but it must be a
229 # subclass of :class:`RPCReply`.
232 def __init__(self, session, async=False, timeout=None, raise_mode="none"):
233 self._session = session
235 for cap in self.DEPENDS:
237 except AttributeError:
240 self._timeout = timeout
241 self._raise_mode = raise_mode
242 self._id = uuid1().urn # Keeps things simple instead of having a class attr that has to be locked
243 self._listener = RPCReplyListener(session)
244 self._listener.register(self._id, self)
247 self._event = Event()
249 def _build(self, subele):
251 ele = new_ele("rpc", {"message-id": self._id}, xmlns=BASE_NS_1_0)
255 def _request(self, op):
256 """Subclasses call this method to make the RPC request.
258 In synchronous mode, waits until the reply is received and returns
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`).
265 :type opspec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
266 :rtype: :class:`RPCReply` (sync) or :class:`RPC` (async)
268 logger.info('Requesting %r' % self.__class__.__name__)
269 req = self._build(op)
270 self._session.send(req)
272 logger.debug('Async request, returning %r', self)
275 logger.debug('Sync request, will wait for timeout=%r' %
277 self._event.wait(self._timeout)
278 if self._event.isSet():
280 # Error that prevented reply delivery
283 if self._reply.error is not None:
284 # <rpc-error>'s [ RPCError ]
285 if self._raise_mode == "all":
286 raise self._reply.error
287 elif (self._raise_mode == "errors" and
288 self._reply.error.type == "error"):
289 raise self._reply.error
292 raise TimeoutExpiredError
294 def request(self, *args, **kwds):
295 "Subclasses implement this method."
296 return self._request(self.SPEC)
298 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
303 if capability not in self._session.server_capabilities:
304 raise MissingCapabilityError('Server does not support [%s]' %
307 def deliver_reply(self, raw):
309 self._reply = self.REPLY_CLS(raw)
312 def deliver_error(self, err):
319 ":class:`RPCReply` element if reply has been received or :const:`None`"
324 """:exc:`Exception` type if an error occured or :const:`None`.
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.
335 "The *message-id* for this RPC"
340 """The :class:`~ncclient.transport.Session` object associated with this
346 """:class:`~threading.Event` that is set when reply has been received or
350 def set_async(self, async=True):
351 """Set asynchronous mode for this RPC."""
353 if async and not session.can_pipeline:
354 raise UserWarning('Asynchronous mode not supported for this device/session')
356 def set_raise_mode(self, mode):
357 assert(choice in ("all", "errors", "none"))
358 self._raise_mode = mode
360 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 self._timeout = timeout
366 #: Whether this RPC is asynchronous
367 is_async = property(fget=lambda self: self._async, fset=set_async)
369 #: Timeout for synchronous waiting
370 timeout = property(fget=lambda self: self._timeout, fset=set_timeout)