Revision cc9af1c3

b/ncclient/__init__.py
13 13
# limitations under the License.
14 14

  
15 15
'''
16
NOTES
16
TODO
17 17
=====
18

  
19
- operations complete
20
- parse into dicts??
21
- code freeze and reST doc
18
* code freeze and reST doc
22 19
'''
23 20

  
24 21
import sys
25 22

  
23
# actually no reason why shouldn't work on 2.5 but that's... untested -- TODO
26 24
if sys.version_info < (2, 6):
27 25
    raise RuntimeError('You need Python 2.6+ for this module.')
28 26

  
29 27
__version__ = "0.05"
30 28

  
31
class NCClientError(Exception):
32
    pass
33

  
34
class TransportError(NCClientError):
35
    pass
36

  
37
class RPCError(NCClientError):
38
    pass
39

  
40
class OperationError(NCClientError):
41
    pass
42

  
43
class ContentError(NCClientError):
44
    pass
b/ncclient/content.py
16 16

  
17 17
from xml.etree import cElementTree as ET
18 18

  
19
iselement = ET.iselement
20
element2string = ET.tostring
21 19

  
22 20
### Namespace-related ###
23 21

  
......
57 55
            if found is not None:
58 56
                break
59 57
    return found
60
    
58

  
61 59

  
62 60
### Build XML using Python data structures ###
63 61

  
......
70 68
        "TODO: docstring"
71 69
        self._root = XMLConverter.build(spec)
72 70
    
73
    def to_string(self, encoding='utf-8'):
71
    def tostring(self, encoding='utf-8'):
74 72
        "TODO: docstring"
75 73
        xml = ET.tostring(self._root, encoding)
76 74
        # some etree versions don't include xml decl with utf-8
......
90 88
            return spec
91 89
        elif isinstance(spec, basestring):
92 90
            return ET.XML(spec)
93
        ## assume isinstance(spec, dict)
91
        # assume isinstance(spec, dict)
94 92
        if 'tag' in spec:
95 93
            ele = ET.Element(spec.get('tag'), spec.get('attributes', {}))
96 94
            ele.text = spec.get('text', '')
......
104 102
            return ele
105 103
        elif 'comment' in spec:
106 104
            return ET.Comment(spec.get('comment'))
105
        # TODO elif DOM rep
107 106
        else:
108 107
            raise ContentError('Invalid tree spec')
109 108
    
110 109
    @staticmethod
111
    def from_string(xml):
110
    def fromstring(xml):
112 111
        return XMLConverter.parse(ET.fromstring(xml))
113 112
    
114 113
    @staticmethod
......
120 119
            'tail': root.tail,
121 120
            'subtree': [ XMLConverter.parse(child) for child in root.getchildren() ]
122 121
        }
122

  
123
## utility functions
124

  
125
iselement = ET.iselement
126

  
127
def isdom(x): return True # TODO
128

  
129
def root_ensured(rep, tag):
130
    if isinstance(rep, basestring):
131
        rep = ET.XML(rep)
132
    err = False
133
    if ((iselement(rep) and (rep.tag not in (tag, qualify(tag))) or (isdom(x)))): 
134
        raise ArgumentError("Expected root element [%s] not found" % tag)
135
    else:
136
        return rep
b/ncclient/glue.py
23 23

  
24 24

  
25 25
def parse_root(raw):
26
    '''Parse the top-level element from a string representing an XML document.
26
    '''Internal use.
27
    Parse the top-level element from XML string.
27 28
    
28 29
    Returns a `(tag, attributes)` tuple, where `tag` is a string representing
29 30
    the qualified name of the root element and `attributes` is an
b/ncclient/manager.py
41 41

  
42 42
class Manager:
43 43
    
44
    'Facade for the API'
44
    "Thin layer of abstraction for the ncclient API."
45 45
    
46 46
    RAISE_ALL = 0
47 47
    RAISE_ERROR = 1
......
51 51
        self._session = session
52 52
        self._raise = rpc_error
53 53

  
54
    def do(self, op, *args, **kwds):
54
    def rpc(self, op, *args, **kwds):
55 55
        op = OPERATIONS[op](self._session)
56 56
        reply = op.request(*args, **kwds)
57 57
        if not reply.ok:
......
75 75
        return reply.data
76 76
    
77 77
    def locked(self, target):
78
        "For use with 'with'. target is the datastore, e.g. 'candidate'"
78
        "Returns a context manager for use withthe 'with' statement.
79
	`target` is the datastore to lock, e.g. 'candidate'"
79 80
        return operations.LockContext(self._session, target)
80
    
81
     
81 82
    get = lambda self, *args, **kwds: self._get('get')
82 83
    
83 84
    get_config = lambda self, *args, **kwds: self._get('get-config')
......
109 110
            pass
110 111
        if self._session.connected: # if that didn't work...
111 112
            self._session.close()
113

  
114
    @property
115
    def session(self, session):
116
	return self._session
117
    
118
    def get_capabilities(self, whose):
119
	if whose in ('manager', 'client'):
120
	    return self._session._client_capabilities
121
	elif whose in ('agent', 'server')
122
	    return self._session._server_capabilities
123

  
124
   
125
    @property
126
    def capabilities(self):
127
        return self._session._client_capabilities
b/ncclient/operations/edit.py
12 12
# See the License for the specific language governing permissions and
13 13
# limitations under the License.
14 14

  
15
from ncclient.rpc import RPC
16 15
from ncclient.content import iselement
17 16

  
17
from rpc import RPC
18

  
18 19
import util
19 20

  
20
"""
21
"""
22 21

  
23
# NOTES
24
# - consider class for helping define <config> for EditConfig??
22
"Operations related to configuration editing"
25 23

  
26 24

  
27 25
class EditConfig(RPC):
28 26
    
29 27
    # tested: no
30
    # combed: no
28
    # combed: yes
31 29
    
32
    SPEC = {
33
        'tag': 'edit-config',
34
        'subtree': []
35
    }
30
    SPEC = {'tag': 'edit-config', 'subtree': []}
36 31
    
37 32
    def request(self, target=None, target_url=None, config=None,
38 33
                default_operation=None, test_option=None, error_option=None):
......
41 36
        subtree = spec['subtree']
42 37
        subtree.append({
43 38
            'tag': 'target',
44
            'subtree': util.store_or_url(target, target_url)
45
            })
46
        subtree.append({
47
            'tag': 'config',
48
            'subtree': config
39
            'subtree': util.store_or_url(target, target_url, self._assert)
49 40
            })
41
        subtree.append(config)
50 42
        if default_operation is not None:
51 43
            subtree.append({
52 44
                'tag': 'default-operation',
......
72 64
    # tested: no
73 65
    # combed: yes
74 66
    
75
    SPEC = {
76
        'tag': 'delete-config',
77
        'subtree': [ { 'tag': 'target', 'subtree': None } ]
78
    }
67
    SPEC = {'tag': 'delete-config', 'subtree': []}
79 68
    
80 69
    def request(self, target=None, target_url=None):
81 70
        spec = DeleteConfig.SPEC.copy()
82
        spec['subtree'][0]['subtree'] = util.store_or_url(target, target_url)
71
        spec['subtree'].append({
72
            'tag': 'target',
73
            'subtree': util.store_or_url(target, target_url, self._assert)
74
            })
83 75
        return self._request(spec)
84 76

  
85 77

  
......
88 80
    # tested: no
89 81
    # combed: yes
90 82
    
91
    SPEC = {
92
        'tag': 'copy-config',
93
        'subtree': []
94
    }
83
    SPEC = {'tag': 'copy-config', 'subtree': []}
95 84
    
96 85
    def request(self, source=None, source_url=None, target=None, target_url=None):
97 86
        spec = CopyConfig.SPEC.copy()
98 87
        spec['subtree'].append({
99
            'tag': 'target',
100
            'subtree': util.store_or_url(source, source_url)
88
            'tag': 'source',
89
            'subtree': util.store_or_url(source, source_url, self._assert)
101 90
            })
102 91
        spec['subtree'].append({
103 92
            'tag': 'target',
104
            'subtree': util.store_or_url(target, target_url)
93
            'subtree': util.store_or_url(target, target_url, self._assert)
105 94
            })
106 95
        return self._request(spec)
107 96

  
......
115 104
    
116 105
    DEPENDS = [':validate']
117 106
    
118
    SPEC = {
119
        'tag': 'validate',
120
        'subtree': []
121
    }
107
    SPEC = {'tag': 'validate', 'subtree': []}
122 108
    
123
    def request(self, source=None, config=None):
124
        util.one_of(source, capability)
125
        spec = SPEC.copy()
126
        if source is not None:
109
    def request(self, source=None, source_url=None, config=None):
110
        util.one_of(source, source_url, config)
111
        spec = Validate.SPEC.copy()
112
        if config is None:
127 113
            spec['subtree'].append({
128 114
                'tag': 'source',
129
                'subtree': {'tag': source}
115
                'subtree': util.store_or_url(source, source_url, self._assert)
130 116
            })
131 117
        else:
132
            spec['subtree'].append({
133
                'tag': 'config',
134
                'subtree': config
135
            })
118
            spec['subtree'].append(config)
136 119
        return self._request(spec)
137 120

  
138 121

  
......
143 126
    
144 127
    DEPENDS = [':candidate']
145 128
    
146
    SPEC = { 'tag': 'commit', 'subtree': [] }
129
    SPEC = {'tag': 'commit', 'subtree': []}
147 130
    
148 131
    def _parse_hook(self):
149 132
        pass
......
151 134
    def request(self, confirmed=False, timeout=None):
152 135
        spec = SPEC.copy()
153 136
        if confirmed:
137
            self._assert(':confirmed-commit')
154 138
            spec['subtree'].append({'tag': 'confirmed'})
155 139
            if timeout is not None:
156 140
                spec['subtree'].append({
b/ncclient/operations/retrieve.py
60 60
    def request(self, filter=None):
61 61
        spec = Get.SPEC.copy()
62 62
        if filter is not None:
63
            spec['subtree'].append(util.build_filter(*filter))
63
            spec['subtree'].append(content.rootchecked(filter. 'filter', 'type'))
64 64
        return self._request(spec)
65 65

  
66 66
class GetConfig(RPC):
......
87 87
            'subtree': util.store_or_url(source, source_url)
88 88
            })
89 89
        if filter is not None:
90
            spec['subtree'].append(util.build_filter(*filter))
90
            spec['subtree'].append(content.rootchecked(filter, 'filter', 'type'))
91 91
        return self._request(spec)
92

  
b/ncclient/operations/rpc/__init__.py
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 rpc import RPC
16
from reply import RPCReply, RPCError
17

  
18
__all__ = [
19
    'RPC',
20
    'RPCReply',
21
    'RPCError'
22
]
b/ncclient/operations/rpc/listener.py
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 Lock
16
from weakref import WeakValueDictionary
17

  
18
from ncclient.glue import Listener
19
from ncclient.content import unqualify as __
20

  
21
import logging
22
logger = logging.getLogger('ncclient.rpc.listener')
23

  
24
class RPCReplyListener(Listener):
25
    
26
    # one instance per session
27
    def __new__(cls, session):
28
        instance = session.get_listener_instance(cls)
29
        if instance is None:
30
            instance = object.__new__(cls)
31
            instance._lock = Lock()
32
            instance._id2rpc = WeakValueDictionary()
33
            instance._pipelined = session.can_pipeline
34
            instance._errback = None
35
            session.add_listener(instance)
36
        return instance
37
    
38
    def register(self, id, rpc):
39
        with self._lock:
40
            self._id2rpc[id] = rpc
41
    
42
    def callback(self, root, raw):
43
        tag, attrs = root
44
        if __(tag) != 'rpc-reply':
45
            return
46
        rpc = None
47
        for key in attrs:
48
            if __(key) == 'message-id':
49
                id = attrs[key]
50
                try:
51
                    with self._lock:
52
                        rpc = self._id2rpc.pop(id)
53
                except KeyError:
54
                    logger.warning('no object registered for message-id: [%s]' % id)
55
                except Exception as e:
56
                    logger.debug('error - %r' % e)
57
                break
58
        else:
59
            if not self._pipelined:
60
                with self._lock:
61
                    assert(len(self._id2rpc) == 1)
62
                    rpc = self._id2rpc.values()[0]
63
                    self._id2rpc.clear()
64
            else:
65
                logger.warning('<rpc-reply> without message-id received: %s' % raw)
66
        logger.debug('delivering to %r' % rpc)
67
        rpc.deliver(raw)
68
    
69
    def set_errback(self, errback):
70
        self._errback = errback
71
    
72
    def errback(self, err):
73
        if self._errback is not None:
74
            self._errback(err)
b/ncclient/operations/rpc/reply.py
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
import ncclient
16

  
17
from xml.etree import cElementTree as ET
18

  
19
from ncclient.content import namespaced_find
20
from ncclient.content import unqualify as __
21

  
22
import logging
23
logger = logging.getLogger('ncclient.rpc.reply')
24

  
25
class RPCReply:
26
    
27
    'NOTES: memory considerations?? storing both raw xml + ET.Element'
28
    
29
    def __init__(self, raw):
30
        self._raw = raw
31
        self._parsed = False
32
        self._root = None
33
        self._errors = []
34
    
35
    def __repr__(self):
36
        return self._raw
37
    
38
    def parse(self):
39
        if self._parsed: return
40
        root = self._root = ET.fromstring(self._raw) # <rpc-reply> element
41
        # per rfc 4741 an <ok/> tag is sent when there are no errors or warnings
42
        ok = namespaced_find(root, 'ok')
43
        if ok is not None:
44
            logger.debug('parsed [%s]' % ok.tag)
45
        else: # create RPCError objects from <rpc-error> elements
46
            error = namespaced_find(root, 'rpc-error')
47
            if error is not None:
48
                logger.debug('parsed [%s]' % error.tag)
49
                for err in root.getiterator(error.tag):
50
                    # process a particular <rpc-error>
51
                    d = {}
52
                    for err_detail in err.getchildren(): # <error-type> etc..
53
                        tag = __(err_detail.tag)
54
                        d[tag] = (err_detail.text.strip() if tag != 'error-info'
55
                                  else ET.tostring(err_detail, 'utf-8'))
56
                    self._errors.append(RPCError(d))
57
        self._parsing_hook(root)
58
        self._parsed = True
59
    
60
    def extract_subtree_xml(self):
61
        return ''.join([ET.tostring(ele)
62
                        for ele in ET.fromstring(self.xml).getchildren()])
63
    
64
    @property
65
    def xml(self):
66
        '<rpc-reply> as returned'
67
        return self._raw
68
    
69
    @property
70
    def ok(self):
71
        if not self._parsed: self.parse()
72
        return not self._errors # empty list => false
73
    
74
    @property
75
    def error(self):
76
        if not self._parsed: self.parse()
77
        if self._errors:
78
            return self._errors[0]
79
        else:
80
            return None
81
    
82
    @property
83
    def errors(self):
84
        'List of RPCError objects. Will be empty if no <rpc-error> elements in reply.'
85
        if not self._parsed: self.parse()
86
        return self._errors
87

  
88

  
89
class RPCError(ncclient.RPCError): # raise it if you like
90
    
91
    def __init__(self, err_dict):
92
        self._dict = err_dict
93
        if self.message is not None:
94
            ncclient.RPCError.__init__(self, self.message)
95
        else:
96
            ncclient.RPCError.__init__(self)
97
    
98
    @property
99
    def raw(self):
100
        return self._element.tostring()
101
    
102
    @property
103
    def type(self):
104
        return self.get('error-type', None)
105
    
106
    @property
107
    def severity(self):
108
        return self.get('error-severity', None)
109
    
110
    @property
111
    def tag(self):
112
        return self.get('error-tag', None)
113
    
114
    @property
115
    def path(self):
116
        return self.get('error-path', None)
117
    
118
    @property
119
    def message(self):
120
        return self.get('error-message', None)
121
    
122
    @property
123
    def info(self):
124
        return self.get('error-info', None)
125

  
126
    ## dictionary interface
127
    
128
    __getitem__ = lambda self, key: self._dict.__getitem__(key)
129
    
130
    __iter__ = lambda self: self._dict.__iter__()
131
    
132
    __contains__ = lambda self, key: self._dict.__contains__(key)
133
    
134
    keys = lambda self: self._dict.keys()
135
    
136
    get = lambda self, key, default: self._dict.get(key, default)
137
        
138
    iteritems = lambda self: self._dict.iteritems()
139
    
140
    iterkeys = lambda self: self._dict.iterkeys()
141
    
142
    itervalues = lambda self: self._dict.itervalues()
143
    
144
    values = lambda self: self._dict.values()
145
    
146
    items = lambda self: self._dict.items()
147
    
148
    __repr__ = lambda self: repr(self._dict)
b/ncclient/operations/rpc/rpc.py
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
from weakref import WeakValueDictionary
18

  
19
from ncclient.content import XMLConverter
20
from ncclient.content import qualify as _
21
from ncclient.content import unqualify as __
22
from ncclient.glue import Listener
23

  
24
from listener import RPCReplyListener
25
from reply import RPCReply
26

  
27
import logging
28
logger = logging.getLogger('ncclient.rpc')
29

  
30
class RPC(object):
31
    
32
    DEPENDS = []
33
    REPLY_CLS = RPCReply
34
    
35
    def __init__(self, session, async=False, timeout=None):
36
        if not session.can_pipeline:
37
            raise UserWarning('Asynchronous mode not supported for this device/session')
38
        self._session = session
39
        try:
40
            for cap in self.DEPENDS:
41
                self._assert(cap)
42
        except AttributeError:
43
            pass        
44
        self._async = async
45
        self._timeout = timeout
46
        self._id = uuid1().urn
47
        self._listener = RPCReplyListener(session)
48
        self._listener.register(self._id, self)
49
        self._reply = None
50
        self._reply_event = Event()
51
    
52
    def _build(self, opspec, encoding='utf-8'):
53
        "TODO: docstring"
54
        spec = {
55
            'tag': _('rpc'),
56
            'attributes': {'message-id': self._id},
57
            'subtree': opspec
58
            }
59
        return XMLConverter(spec).to_string(encoding)
60
    
61
    def _request(self, op):
62
        req = self._build(op)
63
        self._session.send(req)
64
        if self._async:
65
            return self._reply_event
66
        else:
67
            self._reply_event.wait(self._timeout)
68
            if self._reply_event.isSet():
69
                self._reply.parse()
70
                return self._reply
71
            else:
72
                raise ReplyTimeoutError
73
    
74
    def request(self):
75
        return self._request(self.SPEC)
76
    
77
    def _delivery_hook(self):
78
        'For subclasses'
79
        pass
80
    
81
    def _assert(self, capability):
82
        if capability not in self._session.server_capabilities:
83
            raise MissingCapabilityError('Server does not support [%s]' % cap)
84
    
85
    def deliver(self, raw):
86
        self._reply = self.REPLY_CLS(raw)
87
        self._delivery_hook()
88
        self._reply_event.set()
89
    
90
    @property
91
    def has_reply(self):
92
        return self._reply_event.isSet()
93
    
94
    @property
95
    def reply(self):
96
        return self._reply
97
    
98
    @property
99
    def id(self):
100
        return self._id
101
    
102
    @property
103
    def session(self):
104
        return self._session
105
    
106
    @property
107
    def reply_event(self):
108
        return self._reply_event
109
    
110
    def set_async(self, bool): self._async = bool
111
    async = property(fget=lambda self: self._async, fset=set_async)
112
    
113
    def set_timeout(self, timeout): self._timeout = timeout
114
    timeout = property(fget=lambda self: self._timeout, fset=set_timeout)
b/ncclient/operations/util.py
12 12
# See the License for the specific language governing permissions and
13 13
# limitations under the License.
14 14

  
15
'Boilerplate'
15
'Boilerplate ugliness'
16 16

  
17 17
from ncclient import OperationError
18
from ncclient.content import qualify as _
19
from ncclient.content import ensure_root
18 20

  
19
from . import MissingCapabilityError
21
from ncclient.errors import MissingCapabilityError, ArgumentError
20 22

  
21 23
def one_of(*args):
22 24
    'Verifies that only one of the arguments is not None'
......
29 31
                return
30 32
    raise OperationError('Insufficient parameters')
31 33

  
32
def store_or_url(store, url):
34
def store_or_url(store, url, capcheck_func=None):
33 35
    one_of(store, url)
34 36
    node = {}
35 37
    if store is not None:
36 38
        node['tag'] = store
37 39
    else:
40
        if capcheck_func is not None:
41
            capcheck_func(':url') # hmm.. schema check? deem overkill for now
38 42
        node['tag'] = 'url'
39 43
        node['text'] = url
40 44
    return node
41 45

  
42
def build_filter(type, criteria):
43
    filter = {
44
        'tag': 'filter',
45
        'attributes': {'type': type}
46
    }
47
    if type == 'xpath':
48
        filter['attributes']['select'] = criteria
49
    else:
50
        filter['subtree'] = [criteria]
51
    return filter
/dev/null
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 rpc import RPC
16
from reply import RPCReply, RPCError
17

  
18
import ncclient
19

  
20
class ReplyTimeoutError(ncclient.RPCError):
21
    pass
22

  
23
__all__ = [
24
    'RPC',
25
    'RPCReply',
26
    'RPCError',
27
    'ReplyTimeoutError'
28
]
/dev/null
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 Lock
16
from weakref import WeakValueDictionary
17

  
18
from ncclient.glue import Listener
19
from ncclient.content import unqualify as __
20

  
21
import logging
22
logger = logging.getLogger('ncclient.rpc.listener')
23

  
24
class RPCReplyListener(Listener):
25
    
26
    # one instance per session
27
    def __new__(cls, session):
28
        instance = session.get_listener_instance(cls)
29
        if instance is None:
30
            instance = object.__new__(cls)
31
            instance._lock = Lock()
32
            instance._id2rpc = WeakValueDictionary()
33
            instance._pipelined = session.can_pipeline
34
            instance._errback = None
35
            session.add_listener(instance)
36
        return instance
37
    
38
    def register(self, id, rpc):
39
        with self._lock:
40
            self._id2rpc[id] = rpc
41
    
42
    def callback(self, root, raw):
43
        tag, attrs = root
44
        if __(tag) != 'rpc-reply':
45
            return
46
        rpc = None
47
        for key in attrs:
48
            if __(key) == 'message-id':
49
                id = attrs[key]
50
                try:
51
                    with self._lock:
52
                        rpc = self._id2rpc.pop(id)
53
                except KeyError:
54
                    logger.warning('no object registered for message-id: [%s]' % id)
55
                except Exception as e:
56
                    logger.debug('error - %r' % e)
57
                break
58
        else:
59
            if not self._pipelined:
60
                with self._lock:
61
                    assert(len(self._id2rpc) == 1)
62
                    rpc = self._id2rpc.values()[0]
63
                    self._id2rpc.clear()
64
            else:
65
                logger.warning('<rpc-reply> without message-id received: %s' % raw)
66
        logger.debug('delivering to %r' % rpc)
67
        rpc.deliver(raw)
68
    
69
    def set_errback(self, errback):
70
        self._errback = errback
71
    
72
    def errback(self, err):
73
        if self._errback is not None:
74
            self._errback(err)
/dev/null
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
import ncclient
16

  
17
from xml.etree import cElementTree as ET
18

  
19
from ncclient.content import namespaced_find
20
from ncclient.content import unqualify as __
21

  
22
import logging
23
logger = logging.getLogger('ncclient.rpc.reply')
24

  
25
class RPCReply:
26
    
27
    'NOTES: memory considerations?? storing both raw xml + ET.Element'
28
    
29
    def __init__(self, raw):
30
        self._raw = raw
31
        self._parsed = False
32
        self._root = None
33
        self._errors = []
34
    
35
    def __repr__(self):
36
        return self._raw
37
    
38
    def parse(self):
39
        if self._parsed: return
40
        root = self._root = ET.fromstring(self._raw) # <rpc-reply> element
41
        # per rfc 4741 an <ok/> tag is sent when there are no errors or warnings
42
        ok = namespaced_find(root, 'ok')
43
        if ok is not None:
44
            logger.debug('parsed [%s]' % ok.tag)
45
        else: # create RPCError objects from <rpc-error> elements
46
            error = namespaced_find(root, 'rpc-error')
47
            if error is not None:
48
                logger.debug('parsed [%s]' % error.tag)
49
                for err in root.getiterator(error.tag):
50
                    # process a particular <rpc-error>
51
                    d = {}
52
                    for err_detail in err.getchildren(): # <error-type> etc..
53
                        tag = __(err_detail.tag)
54
                        d[tag] = (err_detail.text.strip() if tag != 'error-info'
55
                                  else ET.tostring(err_detail, 'utf-8'))
56
                    self._errors.append(RPCError(d))
57
        self._parsing_hook(root)
58
        self._parsed = True
59
    
60
    def extract_subtree_xml(self):
61
        return ''.join([ET.tostring(ele)
62
                        for ele in ET.fromstring(self.xml).getchildren()])
63
    
64
    @property
65
    def xml(self):
66
        '<rpc-reply> as returned'
67
        return self._raw
68
    
69
    @property
70
    def ok(self):
71
        if not self._parsed: self.parse()
72
        return not self._errors # empty list => false
73
    
74
    @property
75
    def error(self):
76
        if not self._parsed: self.parse()
77
        if self._errors:
78
            return self._errors[0]
79
        else:
80
            return None
81
    
82
    @property
83
    def errors(self):
84
        'List of RPCError objects. Will be empty if no <rpc-error> elements in reply.'
85
        if not self._parsed: self.parse()
86
        return self._errors
87

  
88

  
89
class RPCError(ncclient.RPCError): # raise it if you like
90
    
91
    def __init__(self, err_dict):
92
        self._dict = err_dict
93
        if self.message is not None:
94
            ncclient.RPCError.__init__(self, self.message)
95
        else:
96
            ncclient.RPCError.__init__(self)
97
    
98
    @property
99
    def raw(self):
100
        return self._element.tostring()
101
    
102
    @property
103
    def type(self):
104
        return self.get('error-type', None)
105
    
106
    @property
107
    def severity(self):
108
        return self.get('error-severity', None)
109
    
110
    @property
111
    def tag(self):
112
        return self.get('error-tag', None)
113
    
114
    @property
115
    def path(self):
116
        return self.get('error-path', None)
117
    
118
    @property
119
    def message(self):
120
        return self.get('error-message', None)
121
    
122
    @property
123
    def info(self):
124
        return self.get('error-info', None)
125

  
126
    ## dictionary interface
127
    
128
    __getitem__ = lambda self, key: self._dict.__getitem__(key)
129
    
130
    __iter__ = lambda self: self._dict.__iter__()
131
    
132
    __contains__ = lambda self, key: self._dict.__contains__(key)
133
    
134
    keys = lambda self: self._dict.keys()
135
    
136
    get = lambda self, key, default: self._dict.get(key, default)
137
        
138
    iteritems = lambda self: self._dict.iteritems()
139
    
140
    iterkeys = lambda self: self._dict.iterkeys()
141
    
142
    itervalues = lambda self: self._dict.itervalues()
143
    
144
    values = lambda self: self._dict.values()
145
    
146
    items = lambda self: self._dict.items()
147
    
148
    __repr__ = lambda self: repr(self._dict)
/dev/null
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
from weakref import WeakValueDictionary
18

  
19
from ncclient.content import XMLConverter
20
from ncclient.content import qualify as _
21
from ncclient.content import unqualify as __
22
from ncclient.glue import Listener
23

  
24
from listener import RPCReplyListener
25
from reply import RPCReply
26

  
27
import logging
28
logger = logging.getLogger('ncclient.rpc')
29

  
30
class RPC(object):
31
    
32
    DEPENDS = []
33
    REPLY_CLS = RPCReply
34
    
35
    def __init__(self, session, async=False, timeout=None):
36
        if not session.can_pipeline:
37
            raise UserWarning('Asynchronous mode not supported for this device/session')
38
        self._session = session
39
        try:
40
            for cap in self.DEPENDS:
41
                self._assert(cap)
42
        except AttributeError:
43
            pass        
44
        self._async = async
45
        self._timeout = timeout
46
        self._id = uuid1().urn
47
        self._listener = RPCReplyListener(session)
48
        self._listener.register(self._id, self)
49
        self._reply = None
50
        self._reply_event = Event()
51
    
52
    def _build(self, opspec, encoding='utf-8'):
53
        "TODO: docstring"
54
        spec = {
55
            'tag': _('rpc'),
56
            'attributes': {'message-id': self._id},
57
            'subtree': opspec
58
            }
59
        return XMLConverter(spec).to_string(encoding)
60
    
61
    def _request(self, op):
62
        req = self._build(op)
63
        self._session.send(req)
64
        if self._async:
65
            return self._reply_event
66
        else:
67
            self._reply_event.wait(self._timeout)
68
            if self._reply_event.isSet():
69
                self._reply.parse()
70
                return self._reply
71
            else:
72
                raise ReplyTimeoutError
73
    
74
    def request(self):
75
        return self._request(self.SPEC)
76
    
77
    def _delivery_hook(self):
78
        'For subclasses'
79
        pass
80
    
81
    def _assert(self, capability):
82
        if capability not in self._session.server_capabilities:
83
            raise MissingCapabilityError('Server does not support [%s]' % cap)
84
    
85
    def deliver(self, raw):
86
        self._reply = self.REPLY_CLS(raw)
87
        self._delivery_hook()
88
        self._reply_event.set()
89
    
90
    @property
91
    def has_reply(self):
92
        return self._reply_event.isSet()
93
    
94
    @property
95
    def reply(self):
96
        return self._reply
97
    
98
    @property
99
    def id(self):
100
        return self._id
101
    
102
    @property
103
    def session(self):
104
        return self._session
105
    
106
    @property
107
    def reply_event(self):
108
        return self._reply_event
109
    
110
    def set_async(self, bool): self._async = bool
111
    async = property(fget=lambda self: self._async, fset=set_async)
112
    
113
    def set_timeout(self, timeout): self._timeout = timeout
114
    timeout = property(fget=lambda self: self._timeout, fset=set_timeout)
/dev/null
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
"TODO: docstrings"
16

  
17
from ncclient import NCClientError
18

  
19
class TransportError(NCClientError):
20
    pass
21

  
22
class AuthenticationError(TransportError):
23
    pass
24

  
25
class SessionCloseError(TransportError):
26
    
27
    def __init__(self, in_buf, out_buf=None):
28
        msg = 'Unexpected session close.'
29
        if in_buf:
30
            msg += ' IN_BUFFER: {%s}' % in_buf
31
        if out_buf:
32
            msg += ' OUT_BUFFER: {%s}' % out_buf
33
        SSHError.__init__(self, msg)
34

  
35
class SSHError(TransportError):
36
    pass
37

  
38
class SSHUnknownHostError(SSHError):
39
    
40
    def __init__(self, hostname, key):
41
        from binascii import hexlify
42
        SSHError(self, 'Unknown host key [%s] for [%s]'
43
                 % (hexlify(key.get_fingerprint()), hostname))
44
        self.hostname = hostname
45
        self.key = key

Also available in: Unified diff