Statistics
| Branch: | Tag: | Revision:

root / ncclient / operations / rpc.py @ bbd4ce54

History | View | Annotate | Download (12.8 kB)

1
# Copyright 2009 Shikhar Bhushan
2
#
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
6
#
7
#    http://www.apache.org/licenses/LICENSE-2.0
8
#
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.
14

    
15
from threading import Event, Lock
16
from uuid import uuid1
17

    
18
from ncclient import xml_
19
from ncclient.transport import SessionListener
20

    
21
from errors import OperationError, TimeoutExpiredError, MissingCapabilityError
22

    
23
import logging
24
logger = logging.getLogger('ncclient.operations.rpc')
25

    
26

    
27
class RPCReply:
28

    
29
    """Represents an *<rpc-reply>*. Only concerns itself with whether the
30
    operation was successful.
31

32
    .. note::
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
35
        any subclasses.
36
    """
37

    
38
    def __init__(self, raw):
39
        self._raw = raw
40
        self._parsed = False
41
        self._root = None
42
        self._errors = []
43

    
44
    def __repr__(self):
45
        return self._raw
46

    
47
    def _parsing_hook(self, root):
48
        """Subclass can implement.
49

50
        :type root: :class:`~xml.etree.ElementTree.Element`
51
        """
52
        pass
53

    
54
    def parse(self):
55
        """Parse the *<rpc-reply>*"""
56
        if self._parsed:
57
            return
58
        root = self._root = xml_.xml2ele(self._raw) # <rpc-reply> element
59
        # per rfc 4741 an <ok/> tag is sent when there are no errors or warnings
60
        ok = xml_.find(root, 'ok', nslist=[xml_.BASE_NS, xml_.CISCO_BS])
61
        if ok is not None:
62
            logger.debug('parsed [%s]' % ok.tag)
63
        else: # create RPCError objects from <rpc-error> elements
64
            error = xml_.find(root, 'rpc-error', nslist=[xml_.BASE_NS, xml_.CISCO_BS])
65
            if error is not None:
66
                logger.debug('parsed [%s]' % error.tag)
67
                for err in root.getiterator(error.tag):
68
                    # process a particular <rpc-error>
69
                    d = {}
70
                    for err_detail in err.getchildren(): # <error-type> etc..
71
                        tag = xml_.unqualify(err_detail.tag)
72
                        if tag != 'error-info':
73
                            d[tag] = err_detail.text.strip()
74
                        else:
75
                            d[tag] = xml_.ele2xml(err_detail)
76
                    self._errors.append(RPCError(d))
77
        self._parsing_hook(root)
78
        self._parsed = True
79

    
80
    @property
81
    def xml(self):
82
        "*<rpc-reply>* as returned"
83
        return self._raw
84

    
85
    @property
86
    def ok(self):
87
        "Boolean value indicating if there were no errors."
88
        if not self._parsed:
89
            self.parse()
90
        return not self._errors # empty list => false
91

    
92
    @property
93
    def error(self):
94
        """Short for :attr:`errors` [0]; :const:`None` if there were no errors.
95
        """
96
        if not self._parsed:
97
            self.parse()
98
        if self._errors:
99
            return self._errors[0]
100
        else:
101
            return None
102

    
103
    @property
104
    def errors(self):
105
        """`list` of :class:`RPCError` objects. Will be empty if there were no
106
        *<rpc-error>* elements in reply.
107
        """
108
        if not self._parsed:
109
            self.parse()
110
        return self._errors
111

    
112

    
113
class RPCError(OperationError): # raise it if you like
114

    
115
    """Represents an *<rpc-error>*. It is an instance of :exc:`OperationError`
116
    and can be raised like any other exception."""
117

    
118
    def __init__(self, err_dict):
119
        self._dict = err_dict
120
        if self.message is not None:
121
            OperationError.__init__(self, self.message)
122
        else:
123
            OperationError.__init__(self)
124

    
125
    @property
126
    def type(self):
127
        "`string` representing text of *error-type* element"
128
        return self.get('error-type', None)
129

    
130
    @property
131
    def severity(self):
132
        "`string` representing text of *error-severity* element"
133
        return self.get('error-severity', None)
134

    
135
    @property
136
    def tag(self):
137
        "`string` representing text of *error-tag* element"
138
        return self.get('error-tag', None)
139

    
140
    @property
141
    def path(self):
142
        "`string` or :const:`None`; representing text of *error-path* element"
143
        return self.get('error-path', None)
144

    
145
    @property
146
    def message(self):
147
        "`string` or :const:`None`; representing text of *error-message* element"
148
        return self.get('error-message', None)
149

    
150
    @property
151
    def info(self):
152
        "`string` (XML) or :const:`None`, representing *error-info* element"
153
        return self.get('error-info', None)
154

    
155
    ## dictionary interface
156

    
157
    __getitem__ = lambda self, key: self._dict.__getitem__(key)
158

    
159
    __iter__ = lambda self: self._dict.__iter__()
160

    
161
    __contains__ = lambda self, key: self._dict.__contains__(key)
162

    
163
    keys = lambda self: self._dict.keys()
164

    
165
    get = lambda self, key, default: self._dict.get(key, default)
166

    
167
    iteritems = lambda self: self._dict.iteritems()
168

    
169
    iterkeys = lambda self: self._dict.iterkeys()
170

    
171
    itervalues = lambda self: self._dict.itervalues()
172

    
173
    values = lambda self: self._dict.values()
174

    
175
    items = lambda self: self._dict.items()
176

    
177
    __repr__ = lambda self: repr(self._dict)
178

    
179

    
180
class RPCReplyListener(SessionListener):
181

    
182
    # internal use
183

    
184
    # one instance per session
185
    def __new__(cls, session):
186
        instance = session.get_listener_instance(cls)
187
        if instance is None:
188
            instance = object.__new__(cls)
189
            instance._lock = Lock()
190
            instance._id2rpc = {}
191
            instance._pipelined = session.can_pipeline
192
            session.add_listener(instance)
193
        return instance
194

    
195
    def register(self, id, rpc):
196
        with self._lock:
197
            self._id2rpc[id] = rpc
198

    
199
    def callback(self, root, raw):
200
        tag, attrs = root
201
        if xml_.unqualify(tag) != 'rpc-reply':
202
            return
203
        rpc = None
204
        for key in attrs:
205
            if xml_.unqualify(key) == 'message-id':
206
                id = attrs[key]
207
                try:
208
                    with self._lock:
209
                        rpc = self._id2rpc.pop(id)
210
                except KeyError:
211
                    logger.warning('no object registered for message-id: [%s]' % id)
212
                except Exception as e:
213
                    logger.debug('error - %r' % e)
214
                break
215
        else:
216
            if not self._pipelined:
217
                with self._lock:
218
                    assert(len(self._id2rpc) == 1)
219
                    rpc = self._id2rpc.values()[0]
220
                    self._id2rpc.clear()
221
            else:
222
                logger.warning('<rpc-reply> without message-id received: %s' % raw)
223
        logger.debug('delivering to %r' % rpc)
224
        rpc.deliver_reply(raw)
225

    
226
    def errback(self, err):
227
        try:
228
            for rpc in self._id2rpc.values():
229
                rpc.deliver_error(err)
230
        finally:
231
            self._id2rpc.clear()
232

    
233

    
234
class RPC(object):
235

    
236
    """Base class for all operations.
237

238
    Directly corresponds to *<rpc>* requests. Handles making the request, and
239
    taking delivery of the reply.
240
    """
241

    
242
    #: Subclasses can specify their dependencies on capabilities. List of URI's
243
    # or abbreviated names, e.g. ':writable-running'. These are verified at the
244
    # time of object creation. If the capability is not available, a
245
    # :exc:`MissingCapabilityError` is raised.
246
    DEPENDS = []
247

    
248
    #: Subclasses can specify a different reply class, but it must be a
249
    # subclass of :class:`RPCReply`.
250
    REPLY_CLS = RPCReply
251

    
252
    def __init__(self, session, async=False, timeout=None):
253
        self._session = session
254
        try:
255
            for cap in self.DEPENDS:
256
                self._assert(cap)
257
        except AttributeError:
258
            pass
259
        self._async = async
260
        self._timeout = timeout
261
        # keeps things simple instead of having a class attr that has to be locked
262
        self._id = uuid1().urn
263
        # RPCReplyListener itself makes sure there isn't more than one instance -- i.e. multiton
264
        self._listener = RPCReplyListener(session)
265
        self._listener.register(self._id, self)
266
        self._reply = None
267
        self._error = None
268
        self._event = Event()
269

    
270
    def _build(self, opspec):
271
        # internal
272
        spec = {
273
            'tag': 'rpc',
274
            'attrib': {
275
                'xmlns': xml_.BASE_NS,
276
                'message-id': self._id
277
                },
278
            'subtree': [ opspec ]
279
            }
280
        return xml_.dtree2xml(spec)
281

    
282
    def _request(self, op):
283
        """Subclasses call this method to make the RPC request.
284

285
        In asynchronous mode, returns an :class:`~threading.Event` which is set
286
        when the reply has been received or an error occured. It is prudent,
287
        therefore, to check the :attr:`error` attribute before accesing
288
        :attr:`reply`.
289

290
        Otherwise, waits until the reply is received and returns
291
        :class:`RPCReply`.
292

293
        :arg opspec: :ref:`dtree` for the operation
294
        :type opspec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
295
        :rtype: :class:`~threading.Event` or :class:`RPCReply`
296
        """
297
        logger.debug('request %r with opsepc=%r' % (self, op))
298
        req = self._build(op)
299
        self._session.send(req)
300
        if self._async:
301
            logger.debug('async, returning event')
302
            return self._event
303
        else:
304
            logger.debug('sync, will wait for timeout=%r' % self._timeout)
305
            self._event.wait(self._timeout)
306
            if self._event.isSet():
307
                if self._error:
308
                    raise self._error
309
                self._reply.parse()
310
                return self._reply
311
            else:
312
                raise TimeoutExpiredError
313

    
314
    def request(self, *args, **kwds):
315
        """Subclasses implement this method. Here, the operation is constructed
316
        in :ref:`dtree`, and the result of :meth:`_request` returned."""
317
        raise NotImplementedError
318

    
319
    def _delivery_hook(self):
320
        """Subclasses can implement this method. Will be called after
321
        initialising the :attr:`reply` or :attr:`error` attribute and before
322
        setting the :attr:`event`"""
323
        pass
324

    
325
    def _assert(self, capability):
326
        """Subclasses can use this method to verify that a capability is available
327
        with the NETCONF server, before making a request that requires it. A
328
        :exc:`MissingCapabilityError` will be raised if the capability is not
329
        available."""
330
        if capability not in self._session.server_capabilities:
331
            raise MissingCapabilityError('Server does not support [%s]' %
332
                                         capability)
333

    
334
    def deliver_reply(self, raw):
335
        # internal use
336
        self._reply = self.REPLY_CLS(raw)
337
        self._delivery_hook()
338
        self._event.set()
339

    
340
    def deliver_error(self, err):
341
        # internal use
342
        self._error = err
343
        self._delivery_hook()
344
        self._event.set()
345

    
346
    @property
347
    def reply(self):
348
        ":class:`RPCReply` element if reply has been received or :const:`None`"
349
        return self._reply
350

    
351
    @property
352
    def error(self):
353
        """:exc:`Exception` type if an error occured or :const:`None`.
354

355
        This attribute should be checked if the request was made asynchronously,
356
        so that it can be determined if :attr:`event` being set is because of a
357
        reply or error.
358

359
        .. note::
360
            This represents an error which prevented a reply from being
361
            received. An *<rpc-error>* does not fall in that category -- see
362
            :class:`RPCReply` for that.
363
        """
364
        return self._error
365

    
366
    @property
367
    def id(self):
368
        "The *message-id* for this RPC"
369
        return self._id
370

    
371
    @property
372
    def session(self):
373
        """The :class:`~ncclient.transport.Session` object associated with this
374
        RPC"""
375
        return self._session
376

    
377
    @property
378
    def event(self):
379
        """:class:`~threading.Event` that is set when reply has been received or
380
        error occured."""
381
        return self._event
382

    
383
    def set_async(self, async=True):
384
        """Set asynchronous mode for this RPC."""
385
        self._async = async
386
        if async and not session.can_pipeline:
387
            raise UserWarning('Asynchronous mode not supported for this device/session')
388

    
389
    def set_timeout(self, timeout):
390
        """Set the timeout for synchronous waiting defining how long the RPC
391
        request will block on a reply before raising an error."""
392
        self._timeout = timeout
393

    
394
    #: Whether this RPC is asynchronous
395
    async = property(fget=lambda self: self._async, fset=set_async)
396

    
397
    #: Timeout for synchronous waiting
398
    timeout = property(fget=lambda self: self._timeout, fset=set_timeout)