Revision 94803aaf

b/ncclient/__init__.py
28 28
class OperationError(NCClientError):
29 29
    pass
30 30

  
31
class OperationError(NCClientError):
32
    pass
33

  
31 34
class ContentError(NCClientError):
32 35
    pass
b/ncclient/capabilities.py
40 40
        "TODO: docstring"
41 41
        return repr(self._dict.keys())
42 42
    
43
    def __list__(self):
44
        return self._dict.keys()
45
    
43 46
    def add(self, uri, shorthand=None):
44 47
        "TODO: docstring"
45 48
        if shorthand is None:
b/ncclient/content.py
20 20
### Namespace-related ###
21 21

  
22 22
BASE_NS = 'urn:ietf:params:xml:ns:netconf:base:1.0'
23
NOTIFICATION_NS = 'urn:ietf:params:xml:ns:netconf:notification:1.0'
24 23
# and this is BASE_NS according to cisco devices...
25 24
CISCO_BS = 'urn:ietf:params:netconf:base:1.0'
26 25

  
......
43 42

  
44 43
unqualify = lambda tag: tag[tag.rfind('}')+1:]
45 44

  
46

  
47 45
### Build XML using Python data structures ###
48 46

  
49
class TreeBuilder:
47
class XMLConverter:
50 48
    """Build an ElementTree.Element instance from an XML tree specification
51 49
    based on nested dictionaries. TODO: describe spec
52 50
    """
53 51
    
54 52
    def __init__(self, spec):
55 53
        "TODO: docstring"
56
        self._root = TreeBuilder.build(spec)
54
        self._root = XMLConverter.build(spec)
57 55
    
58 56
    def to_string(self, encoding='utf-8'):
59 57
        "TODO: docstring"
60 58
        xml = ET.tostring(self._root, encoding)
61
        # some etree versions don't always include xml decl
59
        # some etree versions don't always include xml decl e.g. with utf-8
62 60
        # this is a problem with some devices
63 61
        if not xml.startswith('<?xml'):
64
            return '<?xml version="1.0" encoding="%s"?>%s' % (encoding, xml)
62
            return ((u'<?xml version="1.0" encoding="%s"?>'
63
                     % encoding).encode(encoding) + xml)
65 64
        else:
66 65
            return xml
67 66
    
......
82 81
            for child in children:
83 82
                ele.append(TreeBuilder.build(child))
84 83
            return ele
84
        elif 'xml' in spec:
85
            return ET.XML(spec['xml'])
85 86
        elif 'comment' in spec:
86 87
            return ET.Comment(spec.get('comment'))
87 88
        else:
88 89
            raise ValueError('Invalid tree spec')
89

  
90
class Parser:
91
    pass
92

  
93
class PartialParser(Parser):
94
    
95
    pass
96

  
97
class RootParser(Parser):
98
    
99
    pass
b/ncclient/manager.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 capabilities
16
import operations
17
import transport
18

  
19
SESSION_TYPES = {
20
    'ssh': transport.SSHSession
21
}
22

  
23
OPERATIONS = {
24
    'get': operations.Get,
25
    'get-config': operations.GetConfig,
26
    'edit-config': operations.EditConfig,
27
    'copy-config': operations.CopyConfig,
28
    'validate': operations.Validate,
29
    'commit': operations.Commit,
30
    'discard-changes': operations.DiscardChanges,
31
    'delete-config': operations.DeleteConfig,
32
    'lock': operations.Lock,
33
    'unlock': operations.Unlock,
34
    'close_session': operations.CloseSession,
35
    'kill-session': operations.KillSession,
36
}
37

  
38
class Manager(type):
39
    
40
    'Facade for the API'
41
    
42
    def connect(self, session_type, *args, **kwds):
43
        self._session = SESSION_TYPES[session_type](capabilities.CAPABILITIES)
44
        self._session.connect(*args, **kwds)
45
    
46
    def __getattr__(self, name):
47
        if name in OPERATIONS:
48
            return OPERATIONS[name](self._session).request
49
        else:
50
            raise AttributeError
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 rpc import RPC
15
from ncclient.rpc import RPC
16 16

  
17 17
# TODO
18 18

  
......
25 25
'''
26 26

  
27 27
class EditConfig(RPC):
28
    pass
28
    
29
    SPEC = {
30
        'tag': 'edit-config',
31
        'children': [
32
            { 'target': None }
33
        ]
34
    }
35
    
36
    def request(self):
37
        pass
29 38

  
30 39
class CopyConfig(RPC):
31
    pass
40
    
41
    SPEC = {
42
        
43
    }
44
    
45
    def request(self):
46
        pass
32 47

  
33 48
class DeleteConfig(RPC):
34
    pass
49
    
50
    SPEC = {
51
        'tag': 'delete-config',
52
        'children': [
53
            'tag': 'target',
54
            'children': {'tag': None }
55
        ]
56
    }
57
    
58
    def request(self, target=None, targeturl=None):
59
        spec = deepcopy(DeleteConfig.SPEC)
60
        
35 61

  
36 62
class Validate(RPC):
37
    pass
63
    
64
    DEPENDS = ['urn:ietf:params:netconf:capability:validate:1.0']
65
    SPEC = {}
66
    
67
    def request(self):
68
        pass
69

  
38 70

  
39 71
class Commit(RPC):
40
    pass # .confirm() !
72
    
73
    SPEC = {'tag': 'commit'}
74
    
75
    def request(self):
76
        return self._request(Commit.SPEC)
77

  
41 78

  
42 79
class DiscardChanges(RPC):
43
    pass
80
    
81
    DEPENDS = ['urn:ietf:params:netconf:capability:candidate:1.0']
82
    SPEC = {'tag': 'discard-changes'}
83
    
84
    def request(self):
85
        return self._request(DiscardChanges.SPEC)
b/ncclient/operations/lock.py
14 14

  
15 15
'Locking-related NETCONF operations'
16 16

  
17
# TODO - a context manager around some <target> would be real neat
18

  
19
from rpc import RPC
20 17
from copy import deepcopy
21 18

  
19
from ncclient.rpc import RPC
20

  
21
# TODO - a context manager around some <target> would be real neat
22

  
22 23
class Lock(RPC):
23 24
    
24 25
    SPEC = {
b/ncclient/operations/notification.py
17 17
from rpc import RPC
18 18

  
19 19
from ncclient.glue import Listener
20
from ncclient.content import NOTIFICATION_NS
21 20
from ncclient.content import qualify as _
22 21

  
22
NOTIFICATION_NS = 'urn:ietf:params:xml:ns:netconf:notification:1.0'
23

  
23 24
class CreateSubscription(RPC):    
24 25
    
25 26
    SPEC = {
b/ncclient/operations/retrieve.py
12 12
# See the License for the specific language governing permissions and
13 13
# limitations under the License.
14 14

  
15
from rpc import RPC
15
from copy import deepcopy
16

  
17
from ncclient.rpc import RPC
18

  
19
def build_filter(spec, type, criteria):
20
    filter = {
21
        'tag': 'filter',
22
        'attributes': {'type': type}
23
    }
24
    if type=='subtree':
25
        if isinstance(criteria, dict):
26
            filter['children'] = [criteria]
27
        else:
28
            filter['text'] = criteria
29
    elif type=='xpath':
30
        filter['attributes']['select'] = criteria
16 31

  
17 32
class Get(RPC):
33
    
18 34
    SPEC = {
19 35
        'tag': 'get',
20
        'children': None
36
        'children': []
21 37
    }
38
    
39
    def request(self, filter=None):
40
        spec = deepcopy(SPEC)
41
        if filter is not None:
42
            spec['children'].append(build_filter(*filter))
43
        return self._request(spec)
44

  
22 45

  
23 46
class GetConfig(RPC):
24
    pass
47
    
48
    SPEC = {
49
        'tag': 'get-config',
50
        'children': [ { 'tag': 'source', 'children': {'tag': None } } ]
51
    }
52
    
53
    def request(self, source='running', filter=None):
54
        spec = deepcopy(SPEC)
55
        spec['children'][0]['children']['tag'] = source
56
        if filter is not None:
57
            spec['children'].append(build_filter(*filter))
58
        return self._request(spec)
b/ncclient/operations/session.py
14 14

  
15 15
'Session-related NETCONF operations'
16 16

  
17
from rpc import RPC
17
from ncclient.rpc import RPC
18 18
from copy import deepcopy
19 19

  
20 20
class CloseSession(RPC):
b/ncclient/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
17

  
18
class ReplyTimeoutError(Exception):
19
    pass
20

  
21
__all__ = [
22
    'RPC',
23
    'RPCReply',
24
    'ReplyTimeoutError'
25
]
b/ncclient/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
import logging
19
logger = logging.getLogger('ncclient.rpc.listener')
20

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

  
20 20
import logging
21
logger = logging.getLogger('ncclient.operations.reply')
21
logger = logging.getLogger('ncclient.rpc.reply')
22 22

  
23 23
class RPCReply:
24 24
    
25 25
    def __init__(self, raw):
26 26
        self._raw = raw
27 27
        self._parsed = False
28
        self._root = None
28 29
        self._errors = []
29 30
    
30 31
    def __repr__(self):
......
32 33
    
33 34
    def parse(self):
34 35
        if self._parsed: return
35
        root = ET.fromstring(self._raw) # <rpc-reply> element
36
        root = self._root = ET.fromstring(self._raw) # <rpc-reply> element
36 37
        
37 38
        if __(root.tag) != 'rpc-reply':
38 39
            raise ValueError('Root element is not RPC reply')
......
66 67
        return self._raw
67 68
    
68 69
    @property
70
    def root(self):
71
        return self._root
72
    
73
    @property
69 74
    def ok(self):
70 75
        if not self._parsed: self.parse()
71 76
        return not self._errors # empty list => false
b/ncclient/rpc/rpc.py
21 21
from ncclient.content import unqualify as __
22 22
from ncclient.glue import Listener
23 23

  
24
from . import logger
24
from listener import RPCReplyListener
25 25
from reply import RPCReply
26 26

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

  
28
# Cisco does not include message-id attribute in <rpc-reply> in case of an error.
29
# This is messed up however we have to deal with it.
30
# So essentially, there can be only one operation at a time if we are talking to
31
# a Cisco device.
32

  
33
def cisco_check(session):
34
    try:
35
        return session.is_remote_cisco
36
    except AttributeError:
37
        return False
38 30

  
39 31
class RPC(object):
40 32
    
41
    def __init__(self, session, async=False):
42
        if cisco_check(session) and async:
43
            raise UserWarning('Asynchronous mode not supported for Cisco devices')
33
    def __init__(self, session, async=False, timeout=None):
34
        if not session.can_pipeline:
35
            raise UserWarning('Asynchronous mode not supported for this device/session')
44 36
        self._session = session
45 37
        self._async = async
38
        self._timeout = timeout
46 39
        self._id = uuid1().urn
47 40
        self._listener = RPCReplyListener(session)
48 41
        self._listener.register(self._id, self)
49 42
        self._reply = None
50 43
        self._reply_event = Event()
51 44
    
52
    def _build(self, op, encoding='utf-8'):
53
        if isinstance(op, dict):
54
            return self.build_from_spec(self._id, op, encoding)
55
        else:
56
            return self.build_from_string(self._id, op, encoding)
45
    def _build(opspec, encoding='utf-8'):
46
        "TODO: docstring"
47
        spec = {
48
            'tag': _('rpc'),
49
            'attributes': {'message-id': self._id},
50
            'children': opspec
51
            }
52
        return TreeBuilder(spec).to_string(encoding)
57 53
    
58
    def _request(self, op):
54
    def _request(self, op, timeout=None):
59 55
        req = self._build(op)
60 56
        self._session.send(req)
61 57
        if self._async:
62 58
            return self._reply_event
63 59
        else:
64
            self._reply_event.wait()
65
            self._reply.parse()
66
            return self._reply
60
            self._reply_event.wait(timeout)
61
            if self._reply_event.isSet():
62
                self._reply.parse()
63
                return self._reply
64
            else:
65
                raise ReplyTimeoutError
67 66
    
68 67
    def _delivery_hook(self):
69 68
        'For subclasses'
......
83 82
        return self._reply
84 83
    
85 84
    @property
86
    def is_async(self):
87
        return self._async
88
    
89
    @property
90 85
    def id(self):
91 86
        return self._id
92 87
    
......
98 93
    def reply_event(self):
99 94
        return self._reply_event
100 95
    
101
    @staticmethod
102
    def build_from_spec(msgid, opspec, encoding='utf-8'):
103
        "TODO: docstring"
104
        spec = {
105
            'tag': _('rpc'),
106
            'attributes': {'message-id': msgid},
107
            'children': opspec
108
            }
109
        return TreeBuilder(spec).to_string(encoding)
110
    
111
    @staticmethod
112
    def build_from_string(msgid, opstr, encoding='utf-8'):
113
        "TODO: docstring"
114
        decl = '<?xml version="1.0" encoding="%s"?>' % encoding
115
        doc = (u'<rpc message-id="%s" xmlns="%s">%s</rpc>' %
116
               (msgid, BASE_NS, opstr)).encode(encoding)
117
        return '%s%s' % (decl, doc)
118

  
119

  
120
class RPCReplyListener(Listener):
121
    
122
    # TODO - determine if need locking
123
    
124
    # one instance per session
125
    def __new__(cls, session):
126
        instance = session.get_listener_instance(cls)
127
        if instance is None:
128
            instance = object.__new__(cls)
129
            instance._id2rpc = WeakValueDictionary()
130
            instance._cisco = cisco_check(session)
131
            instance._errback = None
132
            session.add_listener(instance)
133
        return instance
134
    
135
    def __str__(self):
136
        return 'RPCReplyListener'
137
    
138
    def set_errback(self, errback):
139
        self._errback = errback
140

  
141
    def register(self, id, rpc):
142
        self._id2rpc[id] = rpc
143
    
144
    def callback(self, root, raw):
145
        tag, attrs = root
146
        if __(tag) != 'rpc-reply':
147
            return
148
        rpc = None
149
        for key in attrs:
150
            if __(key) == 'message-id':
151
                id = attrs[key]
152
                try:
153
                    rpc = self._id2rpc.pop(id)
154
                except KeyError:
155
                    logger.warning('[RPCReplyListener.callback] no object '
156
                                   + 'registered for message-id: [%s]' % id)
157
                except Exception as e:
158
                    logger.debug('[RPCReplyListener.callback] error - %r' % e)
159
                break
160
        else:
161
            if self._cisco:
162
                assert(len(self._id2rpc) == 1)
163
                rpc = self._id2rpc.values()[0]
164
                self._id2rpc.clear()
165
            else:
166
                logger.warning('<rpc-reply> without message-id received: %s' % raw)
167
        logger.debug('[RPCReplyListener.callback] delivering to %r' % rpc)
168
        rpc.deliver(raw)
96
    def set_async(self, bool): self._async = bool
97
    async = property(fget=lambda self: self._async, fset=set_async)
169 98
    
170
    def errback(self, err):
171
        if self._errback is not None:
172
            self._errback(err)
99
    def set_timeout(self, timeout): self._timeout = timeout
100
    timeout = property(fget=lambda self: self._timeout, fset=set_timeout)
b/ncclient/transport/session.py
88 88
    @property
89 89
    def id(self):
90 90
        return self._id
91
    
92
    @property
93
    def can_pipeline(self):
94
        return True
b/ncclient/transport/ssh.py
289 289
        return self._transport
290 290
    
291 291
    @property
292
    def is_remote_cisco(self):
293
        return 'Cisco' in self._transport.remote_version
292
    def can_pipeline(self):
293
        if 'Cisco' in self._transport.remote_version:
294
            return False
295
        # elif ..
296
        return True

Also available in: Unified diff