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
17 from weakref import WeakValueDictionary
19 from ncclient import content
20 from ncclient.transport import SessionListener
22 from errors import OperationError, TimeoutExpiredError, MissingCapabilityError
25 logger = logging.getLogger('ncclient.operations.rpc')
30 """Represents an *<rpc-reply>*. Only concerns itself with whether the
31 operation was successful.
34 If the reply has not yet been parsed there is an implicit, one-time
35 parsing overhead to accessing the attributes defined by this class and
39 def __init__(self, raw):
48 def _parsing_hook(self, root):
49 """Subclass can implement.
51 :type root: :class:`~xml.etree.ElementTree.Element`
56 """Parse the *<rpc-reply>*"""
59 root = self._root = content.xml2ele(self._raw) # <rpc-reply> element
60 # per rfc 4741 an <ok/> tag is sent when there are no errors or warnings
61 ok = content.find(root, 'ok', nslist=[content.BASE_NS, content.CISCO_BS])
63 logger.debug('parsed [%s]' % ok.tag)
64 else: # create RPCError objects from <rpc-error> elements
65 error = content.find(root, 'rpc-error', nslist=[content.BASE_NS, content.CISCO_BS])
67 logger.debug('parsed [%s]' % error.tag)
68 for err in root.getiterator(error.tag):
69 # process a particular <rpc-error>
71 for err_detail in err.getchildren(): # <error-type> etc..
72 tag = content.unqualify(err_detail.tag)
73 if tag != 'error-info':
74 d[tag] = err_detail.text.strip()
76 d[tag] = content.ele2xml(err_detail)
77 self._errors.append(RPCError(d))
78 self._parsing_hook(root)
83 "*<rpc-reply>* as returned"
88 "Boolean value indicating if there were no errors."
91 return not self._errors # empty list => false
95 """Short for :attr:`errors` [0]; :const:`None` if there were no errors.
100 return self._errors[0]
106 """`list` of :class:`RPCError` objects. Will be empty if there were no
107 *<rpc-error>* elements in reply.
114 class RPCError(OperationError): # raise it if you like
116 """Represents an *<rpc-error>*. It is an instance of :exc:`OperationError`
117 so it can be raised like any other exception."""
119 def __init__(self, err_dict):
120 self._dict = err_dict
121 if self.message is not None:
122 OperationError.__init__(self, self.message)
124 OperationError.__init__(self)
128 "`string` represeting *error-type* element"
129 return self.get('error-type', None)
133 "`string` represeting *error-severity* element"
134 return self.get('error-severity', None)
138 "`string` represeting *error-tag* element"
139 return self.get('error-tag', None)
143 "`string` or :const:`None`; represeting *error-path* element"
144 return self.get('error-path', None)
148 "`string` or :const:`None`; represeting *error-message* element"
149 return self.get('error-message', None)
153 "`string` or :const:`None`, represeting *error-info* element"
154 return self.get('error-info', None)
156 ## dictionary interface
158 __getitem__ = lambda self, key: self._dict.__getitem__(key)
160 __iter__ = lambda self: self._dict.__iter__()
162 __contains__ = lambda self, key: self._dict.__contains__(key)
164 keys = lambda self: self._dict.keys()
166 get = lambda self, key, default: self._dict.get(key, default)
168 iteritems = lambda self: self._dict.iteritems()
170 iterkeys = lambda self: self._dict.iterkeys()
172 itervalues = lambda self: self._dict.itervalues()
174 values = lambda self: self._dict.values()
176 items = lambda self: self._dict.items()
178 __repr__ = lambda self: repr(self._dict)
181 class RPCReplyListener(SessionListener):
185 # one instance per session
186 def __new__(cls, session):
187 instance = session.get_listener_instance(cls)
189 instance = object.__new__(cls)
190 instance._lock = Lock()
191 instance._id2rpc = WeakValueDictionary()
192 instance._pipelined = session.can_pipeline
193 session.add_listener(instance)
196 def register(self, id, rpc):
198 self._id2rpc[id] = rpc
200 def callback(self, root, raw):
202 if content.unqualify(tag) != 'rpc-reply':
206 if content.unqualify(key) == 'message-id':
210 rpc = self._id2rpc.pop(id)
212 logger.warning('no object registered for message-id: [%s]' % id)
213 except Exception as e:
214 logger.debug('error - %r' % e)
217 if not self._pipelined:
219 assert(len(self._id2rpc) == 1)
220 rpc = self._id2rpc.values()[0]
223 logger.warning('<rpc-reply> without message-id received: %s' % raw)
224 logger.debug('delivering to %r' % rpc)
225 rpc.deliver_reply(raw)
227 def errback(self, err):
228 for rpc in self._id2rpc.values():
229 rpc.deliver_error(err)
234 """Base class for all operations.
236 Directly corresponds to *<rpc>* requests. Handles making the request, and
237 taking delivery of the reply.
240 # : Subclasses can specify their dependencies on capabilities. List of URI's
241 # or abbreviated names, e.g. ':writable-running'. These are verified at the
242 # time of object creation. If the capability is not available, a
243 # :exc:`MissingCapabilityError` is raised.
246 # : Subclasses can specify a different reply class, but it must be a
247 # subclass of :class:`RPCReply`.
250 def __init__(self, session, async=False, timeout=None):
251 self._session = session
253 for cap in self.DEPENDS:
255 except AttributeError:
258 self._timeout = timeout
259 # keeps things simple instead of having a class attr that has to be locked
260 self._id = uuid1().urn
261 # RPCReplyListener itself makes sure there isn't more than one instance -- i.e. multiton
262 self._listener = RPCReplyListener(session)
263 self._listener.register(self._id, self)
266 self._event = Event()
268 def _build(self, opspec):
271 'tag': content.qualify('rpc'),
272 'attrib': {'message-id': self._id},
273 'subtree': [ opspec ]
275 return content.dtree2xml(spec)
277 def _request(self, op):
278 """Subclasses call this method to make the RPC request.
280 In asynchronous mode, returns an :class:`~threading.Event` which is set
281 when the reply has been received or an error occured. It is prudent,
282 therefore, to check the :attr:`error` attribute before accesing
285 Otherwise, waits until the reply is received and returns
288 :arg opspec: :ref:`dtree` for the operation
289 :type opspec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
290 :rtype: :class:`~threading.Event` or :class:`RPCReply`
292 logger.debug('request %r with opsepc=%r' % (self, op))
293 req = self._build(op)
294 self._session.send(req)
296 logger.debug('async, returning event')
299 logger.debug('sync, will wait for timeout=%r' % self._timeout)
300 self._event.wait(self._timeout)
301 if self._event.isSet():
307 raise TimeoutExpiredError
309 def request(self, *args, **kwds):
310 """Subclasses implement this method. Here, the operation is constructed
311 in :ref:`dtree`, and the result of :meth:`_request` returned."""
312 raise NotImplementedError
314 def _delivery_hook(self):
315 """Subclasses can implement this method. Will be called after
316 initialising the :attr:`reply` or :attr:`error` attribute and before
317 setting the :attr:`event`"""
320 def _assert(self, capability):
321 """Subclasses can use this method to verify that a capability is available
322 with the NETCONF server, before making a request that requires it. A
323 :exc:`MissingCapabilityError` will be raised if the capability is not
325 if capability not in self._session.server_capabilities:
326 raise MissingCapabilityError('Server does not support [%s]' % cap)
328 def deliver_reply(self, raw):
330 self._reply = self.REPLY_CLS(raw)
331 self._delivery_hook()
334 def deliver_error(self, err):
337 self._delivery_hook()
342 ":class:`RPCReply` element if reply has been received or :const:`None`"
347 """:exc:`Exception` type if an error occured or :const:`None`.
349 This attribute should be checked if the request was made asynchronously,
350 so that it can be determined if :attr:`event` being set is because of a
354 This represents an error which prevented a reply from being
355 received. An *<rpc-error>* does not fall in that category -- see
356 :class:`RPCReply` for that.
362 "The *message-id* for this RPC"
367 """The :class:`~ncclient.transport.Session` object associated with this
373 """:class:`~threading.Event` that is set when reply has been received or
377 def set_async(self, async=True):
378 """Set asynchronous mode for this RPC."""
380 if async and not session.can_pipeline:
381 raise UserWarning('Asynchronous mode not supported for this device/session')
383 def set_timeout(self, timeout):
384 """Set the timeout for synchronous waiting defining how long the RPC
385 request will block on a reply before raising an error."""
386 self._timeout = timeout
388 #: Whether this RPC is asynchronous
389 async = property(fget=lambda self: self._async, fset=set_async)
391 #: Timeout for synchronous waiting
392 timeout = property(fget=lambda self: self._timeout, fset=set_timeout)