Revision 0cdb8b3c

b/ncclient/capabilities.py
12 12
# See the License for the specific language governing permissions and
13 13
# limitations under the License.
14 14

  
15
def abbreviate(uri):
16
    if uri.startswith('urn:ietf:params:netconf:capability:'):
17
        return (':' + uri.split(':')[5])
18

  
19
def schemes(uri):
20
    return uri.partition("?scheme=")[2].split(',')
15 21

  
16 22
class Capabilities:
17 23
    
......
25 31
            self._dict = capabilities
26 32
        elif isinstance(capabilities, list):
27 33
            for uri in capabilities:
28
                self._dict[uri] = Capabilities.guess_shorthand(uri)
34
                self._dict[uri] = abbreviate(uri)
29 35
    
30 36
    def __contains__(self, key):
31 37
        return ( key in self._dict ) or ( key in self._dict.values() )
......
41 47
    
42 48
    def add(self, uri, shorthand=None):
43 49
        if shorthand is None:
44
            shorthand = Capabilities.guess_shorthand(uri)
50
            shorthand = abbreviate(uri)
45 51
        self._dict[uri] = shorthand
46 52
    
47 53
    set = add
......
54 60
                if self._dict[uri] == key:
55 61
                    del self._dict[uri]
56 62
                    break
57
    
58
    @staticmethod
59
    def guess_shorthand(uri):
60
        if uri.startswith('urn:ietf:params:netconf:capability:'):
61
            return (':' + uri.split(':')[5])
62 63

  
64
# : the capabilities currently supported by ncclient
63 65
CAPABILITIES = Capabilities([
64 66
    'urn:ietf:params:netconf:base:1.0',
65 67
    'urn:ietf:params:netconf:capability:writable-running:1.0',
......
67 69
    'urn:ietf:params:netconf:capability:confirmed-commit:1.0',
68 70
    'urn:ietf:params:netconf:capability:rollback-on-error:1.0',
69 71
    'urn:ietf:params:netconf:capability:startup:1.0',
70
    'urn:ietf:params:netconf:capability:url:1.0?scheme=http,ftp,file',
72
    'urn:ietf:params:netconf:capability:url:1.0?scheme=http,ftp,file,https,sftp',
71 73
    'urn:ietf:params:netconf:capability:validate:1.0',
72 74
    'urn:ietf:params:netconf:capability:xpath:1.0',
73
    'urn:ietf:params:netconf:capability:notification:1.0',
74
    'urn:ietf:params:netconf:capability:interleave:1.0'
75
])
75
    #'urn:ietf:params:netconf:capability:notification:1.0', # TODO
76
    #'urn:ietf:params:netconf:capability:interleave:1.0' # theoretically already supported
77
])
b/ncclient/content.py
23 23
### Namespace-related
24 24

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

  
......
39 40

  
40 41
qualify = lambda tag, ns=BASE_NS: tag if ns is None else '{%s}%s' % (ns, tag)
41 42

  
43
# deprecated
42 44
multiqualify = lambda tag, nslist=(BASE_NS, CISCO_BS): [qualify(tag, ns) for ns in nslist]
43 45

  
44 46
unqualify = lambda tag: tag[tag.rfind('}')+1:]
......
72 74
            raise ContentError('Invalid tree spec')
73 75
    
74 76
    @staticmethod
75
    def XML(spec, encoding='utf-8'):
77
    def XML(spec, encoding='UTF-8'):
76 78
        return Element.XML(DictTree.Element(spec), encoding)
77 79

  
78 80
class Element:
......
88 90
        }
89 91
    
90 92
    @staticmethod
91
    def XML(ele, encoding='utf-8'):
93
    def XML(ele, encoding='UTF-8'):
92 94
        xml = ET.tostring(ele, encoding)
93 95
        if xml.startswith('<?xml'):
94 96
            return xml
......
116 118

  
117 119
iselement = ET.iselement
118 120

  
119
def find(ele, tag, strict=False):
120
    """In strict mode, doesn't workaround Cisco implementations sending incorrectly
121
    namespaced XML. Supply qualified tag name if using strict mode.
121
def find(ele, tag, strict=True, nslist=[BASE_NS, CISCO_BS]):
122
    """In strict mode, doesn't work around Cisco implementations sending incorrectly namespaced XML. Supply qualified tag name if using strict mode.
122 123
    """
123 124
    if strict:
124 125
        return ele.find(tag)
......
126 127
        for qname in multiqualify(tag):
127 128
            found = ele.find(qname)
128 129
            if found is not None:
129
                return found
130
                return found        
130 131

  
131 132
def parse_root(raw):
132
    '''Parse the top-level element from XML string.
133
    
134
    Returns a `(tag, attributes)` tuple, where `tag` is a string representing
135
    the qualified name of the root element and `attributes` is an
136
    `{attribute: value}` dictionary.
137
    '''
133
    """
134
    """
138 135
    fp = StringIO(raw[:1024]) # this is a guess but start element beyond 1024 bytes would be a bit absurd
139 136
    for event, element in ET.iterparse(fp, events=('start',)):
140 137
        return (element.tag, element.attrib)
141 138

  
142 139
def validated_element(rep, tag, attrs=None):
140
    """
141
    """
143 142
    ele = dtree2ele(rep)
144 143
    if ele.tag not in (tag, qualify(tag)):
145 144
        raise ContentError("Required root element [%s] not found" % tag)
b/ncclient/manager.py
17 17
import transport
18 18

  
19 19

  
20
def connect_ssh(*args, **kwds):
20
def ssh_connect(*args, **kwds):
21 21
    session = transport.SSHSession(capabilities.CAPABILITIES)
22 22
    session.load_system_host_keys()
23 23
    session.connect(*args, **kwds)
24 24
    return Manager(session)
25 25

  
26
connect = connect_ssh # default session type
26
connect = ssh_connect # default session type
27 27

  
28 28
RAISE_ALL, RAISE_ERROR, RAISE_NONE = range(3)
29 29

  
30 30
class Manager:
31 31
    
32
    "Thin layer of abstraction for the API."
32
    "Thin layer of abstraction for the ncclient API."
33 33
    
34 34
    def __init__(self, session, rpc_error=RAISE_ALL):
35 35
        self._session = session
b/ncclient/operations/retrieve.py
28 28
    def _parsing_hook(self, root):
29 29
        self._data = None
30 30
        if not self._errors:
31
            self._data = content.find(root, 'data')
31
            self._data = content.find(root, 'data', strict=False)
32 32
    
33 33
    @property
34 34
    def data(self):
b/ncclient/operations/rpc.py
17 17
from weakref import WeakValueDictionary
18 18

  
19 19
from ncclient import content
20
from ncclient.transport import SessionListener
20 21

  
21 22
from errors import OperationError
22 23

  
......
45 46
            return
46 47
        root = self._root = content.xml2ele(self._raw) # <rpc-reply> element
47 48
        # per rfc 4741 an <ok/> tag is sent when there are no errors or warnings
48
        ok = content.find(root, 'ok')
49
        ok = content.find(root, 'data', strict=False)
49 50
        if ok is not None:
50 51
            logger.debug('parsed [%s]' % ok.tag)
51 52
        else: # create RPCError objects from <rpc-error> elements
52
            error = content.find(root, 'rpc-error')
53
            error = content.find(root, 'data', strict=False)
53 54
            if error is not None:
54 55
                logger.debug('parsed [%s]' % error.tag)
55 56
                for err in root.getiterator(error.tag):
......
151 152
    __repr__ = lambda self: repr(self._dict)
152 153

  
153 154

  
154
class RPCReplyListener(object):
155
class RPCReplyListener(SessionListener):
155 156
    
156 157
    # one instance per session
157 158
    def __new__(cls, session):
......
161 162
            instance._lock = Lock()
162 163
            instance._id2rpc = WeakValueDictionary()
163 164
            instance._pipelined = session.can_pipeline
164
            instance._errback = None
165 165
            session.add_listener(instance)
166 166
        return instance
167 167
    
168 168
    def register(self, id, rpc):
169 169
        with self._lock:
170 170
            self._id2rpc[id] = rpc
171

  
172
    def set_errback(self, errback):
173
        self._errback = errback
174 171
    
175 172
    def callback(self, root, raw):
176 173
        tag, attrs = root
......
200 197
        rpc.deliver(raw)
201 198
    
202 199
    def errback(self, err):
203
        if self._errback is not None:
204
            self._errback(err)
200
        for rpc in self._id2rpc.values():
201
            rpc.error(err)
205 202

  
206 203

  
207 204
class RPC(object):
......
241 238
        req = self._build(op)
242 239
        self._session.send(req)
243 240
        if self._async:
244
            return self._reply_event
241
            return (self._reply_event, self._error_event)
245 242
        else:
246 243
            self._reply_event.wait(self._timeout)
247
            if self._reply_event.isSet():
244
            if self._reply_event.is_set():
245
                if self._error:
246
                    raise self._error
248 247
                self._reply.parse()
249 248
                return self._reply
250 249
            else:
......
266 265
        self._delivery_hook()
267 266
        self._reply_event.set()
268 267
    
268
    def error(self, err):
269
        self._error = err
270
        self._reply_event.set()
271
    
269 272
    @property
270 273
    def has_reply(self):
271
        return self._reply_event.isSet()
274
        return self._reply_event.is_set()
272 275
    
273 276
    @property
274 277
    def reply(self):
b/ncclient/operations/subscribe.py
17 17
from ncclient.glue import Listener
18 18
from ncclient.content import qualify as _
19 19

  
20
NOTIFICATION_NS = 'urn:ietf:params:xml:ns:netconf:notification:1.0'
21

  
22 20
# TODO when can actually test it...
23 21

  
24 22
class CreateSubscription(RPC):    
b/ncclient/transport/session.py
22 22
logger = logging.getLogger('ncclient.transport.session')
23 23

  
24 24
class Session(Thread):
25
    "This is a base class for use by protocol implementations"
25 26
    
26 27
    def __init__(self, capabilities):
27 28
        Thread.__init__(self)
......
38 39
                     (self, self._client_capabilities))
39 40
    
40 41
    def _dispatch_message(self, raw):
41
        "TODO: docstring"
42 42
        try:
43 43
            root = content.parse_root(raw)
44 44
        except Exception as e:
......
54 54
                logger.warning('[error] %r' % e)
55 55
    
56 56
    def _dispatch_error(self, err):
57
        "TODO: docstring"
58 57
        with self._lock:
59 58
            listeners = list(self._listeners)
60 59
        for l in listeners:
......
87 86
        self.remove_listener(listener)
88 87
        if error[0]:
89 88
            raise error[0]
90
        logger.info('initialized: session-id=%s | server_capabilities=%s' %
91
                     (self._id, self._server_capabilities))
89
        logger.info('initialized: session-id=%s | server_capabilities=%s' % (self._id, self._server_capabilities))
92 90
    
93 91
    def add_listener(self, listener):
92
        """Register a listener that will be notified of incoming messages and errors.
93
        
94
        :type listener: :class:`SessionListener`
95
        """
94 96
        logger.debug('installing listener %r' % listener)
95 97
        if not isinstance(listener, SessionListener):
96 98
            raise SessionError("Listener must be a SessionListener type")
......
98 100
            self._listeners.add(listener)
99 101
    
100 102
    def remove_listener(self, listener):
103
        "Unregister some listener; ignoring if the listener was never registered."
101 104
        logger.debug('discarding listener %r' % listener)
102 105
        with self._lock:
103 106
            self._listeners.discard(listener)
104 107
    
105 108
    def get_listener_instance(self, cls):
109
        """If a listener of the specified type is registered, returns it. This is useful when it is desirable to have only one instance of a particular type per session, i.e. a multiton.
110
        
111
        :type cls: :class:`type`
112
        :rtype: :class:`SessionListener` or :const:`None`
113
        """
106 114
        with self._lock:
107 115
            for listener in self._listeners:
108 116
                if isinstance(listener, cls):
109 117
                    return listener
110 118
    
111
    def connect(self, *args, **kwds):
119
    def connect(self, *args, **kwds): # subclass implements
112 120
        raise NotImplementedError
113 121

  
114
    def run(self):
122
    def run(self): # subclass implements
115 123
        raise NotImplementedError
116 124
    
117 125
    def send(self, message):
126
        """
127
        :param message: XML document
128
        :type message: string
129
        """
118 130
        logger.debug('queueing %s' % message)
119 131
        self._q.put(message)
120 132
    
121 133
    ### Properties
122
    
134

  
135
    @property
136
    def connected(self):
137
        ":rtype: bool"
138
        return self._connected
139

  
123 140
    @property
124 141
    def client_capabilities(self):
142
        ":rtype: :class:`Capabilities`"
125 143
        return self._client_capabilities
126 144
    
127 145
    @property
128 146
    def server_capabilities(self):
147
        ":rtype: :class:`Capabilities` or :const:`None`"
129 148
        return self._server_capabilities
130 149
    
131 150
    @property
132
    def connected(self):
133
        return self._connected
134
    
135
    @property
136 151
    def id(self):
137
        "`session-id` if session is initialized, :const:`None` otherwise"
152
        ":rtype: :obj:`string` or :const:`None`"
138 153
        return self._id
139 154
    
140 155
    @property
141 156
    def can_pipeline(self):
157
        ":rtype: :obj:`bool`"
142 158
        return True
143 159

  
144 160

  
145 161
class SessionListener(object):
146 162
    
163
    """'Listen' to incoming messages on a NETCONF :class:`Session`
164
    
165
    .. note::
166
        Avoid computationally intensive tasks in the callbacks.
167
    """
168
    
147 169
    def callback(self, root, raw):
170
        """Called when a new XML document is received. The `root` argument allows the callback to determine whether it wants to further process the document.
171
        
172
        :param root: tuple of (tag, attrs) where tag is the qualified name of the root element and attrs is a dictionary of its attributes (also qualified names)
173
        :param raw: XML document
174
        :type raw: string
175
        """
148 176
        raise NotImplementedError
149 177
    
150 178
    def errback(self, ex):
179
        """Called when an error occurs.
180
        
181
        :type ex: :class:`Exception`
182
        """
151 183
        raise NotImplementedError
152 184

  
153 185

  

Also available in: Unified diff