Revision 9667bcb2

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
_capability_map = {
16
    'urn:liberouter:params:netconf:capability:power-control:1.0':
17
        [':power-control', ':power-control:1.0']
18
}
15
#_capability_map = {
16
#    "urn:liberouter:params:netconf:capability:power-control:1.0":
17
#        [":power-control", ":power-control:1.0"]
18
#}
19 19

  
20 20
def _abbreviate(uri):
21
    if uri.startswith('urn:ietf:params:netconf:'):
22
        splitted = uri.split(':')
23
        if ':capability:' in uri:
24
            return [ ':' + splitted[5], ':' + splitted[5] + ':' + splitted[6] ]
25
        elif ':base:' in uri:
26
            return [ ':base', ':base' + ':'+ splitted[5] ]
27
    elif uri in _capability_map:
28
        return _capability_map[uri]
21
    if uri.startswith("urn:ietf:params:netconf:"):
22
        splitted = uri.split(":")
23
        if ":capability:" in uri:
24
            return [ ":" + splitted[5], ":" + splitted[5] + ":" + splitted[6] ]
25
        elif ":base:" in uri:
26
            return [ ":base", ":base" + ":" + splitted[5] ]
27
    #elif uri in _capability_map:
28
    #    return _capability_map[uri]
29 29
    return []
30 30

  
31 31
def schemes(url_uri):
32 32
    """Given a URI that has a *scheme* query string (i.e. *:url* capability
33 33
    URI), will return a list of supported schemes.
34 34
    """
35
    return url_uri.partition("?scheme=")[2].split(',')
35
    return url_uri.partition("?scheme=")[2].split(",")
36 36

  
37 37
class Capabilities:
38 38

  
......
98 98

  
99 99
#: :class:`Capabilities` object representing the capabilities currently supported by NCClient
100 100
CAPABILITIES = Capabilities([
101
    'urn:ietf:params:netconf:base:1.0',
102
    'urn:ietf:params:netconf:capability:writable-running:1.0',
103
    'urn:ietf:params:netconf:capability:candidate:1.0',
104
    'urn:ietf:params:netconf:capability:confirmed-commit:1.0',
105
    'urn:ietf:params:netconf:capability:rollback-on-error:1.0',
106
    'urn:ietf:params:netconf:capability:startup:1.0',
107
    'urn:ietf:params:netconf:capability:url:1.0?scheme=http,ftp,file,https,sftp',
108
    'urn:ietf:params:netconf:capability:validate:1.0',
109
    'urn:ietf:params:netconf:capability:xpath:1.0',
110
    'urn:liberouter:params:netconf:capability:power-control:1.0'
101
    "urn:ietf:params:netconf:base:1.0",
102
    "urn:ietf:params:netconf:capability:writable-running:1.0",
103
    "urn:ietf:params:netconf:capability:candidate:1.0",
104
    "urn:ietf:params:netconf:capability:confirmed-commit:1.0",
105
    "urn:ietf:params:netconf:capability:rollback-on-error:1.0",
106
    "urn:ietf:params:netconf:capability:startup:1.0",
107
    "urn:ietf:params:netconf:capability:url:1.0?scheme=http,ftp,file,https,sftp",
108
    "urn:ietf:params:netconf:capability:validate:1.0",
109
    "urn:ietf:params:netconf:capability:xpath:1.0",
110
    "urn:liberouter:params:netconf:capability:power-control:1.0"
111
    "urn:ietf:params:netconf:capability:interleave:1.0"
111 112
    #'urn:ietf:params:netconf:capability:notification:1.0', # TODO
112
    #'urn:ietf:params:netconf:capability:interleave:1.0' # theoretically already supported
113
    
113 114
])
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 copy import deepcopy
16

  
17
from ncclient import xml_
15
from ncclient.xml_ import *
18 16

  
19 17
from rpc import RPC
20 18

  
21 19
import util
22 20

  
23 21
import logging
24
logger = logging.getLogger('ncclient.operations.edit')
22
logger = logging.getLogger("ncclient.operations.edit")
25 23

  
26 24
"Operations related to changing device configuration"
27 25

  
28 26
class EditConfig(RPC):
29 27

  
30 28
    "*<edit-config>* RPC"
31

  
32
    SPEC = {'tag': 'edit-config', 'subtree': []}
33

  
29
    
34 30
    def request(self, target, config, default_operation=None, test_option=None,
35 31
                error_option=None):
36 32
        """
......
51 47

  
52 48
        :seealso: :ref:`return`
53 49
        """
54
        spec = deepcopy(EditConfig.SPEC)
55
        subtree = spec['subtree']
56
        subtree.append(util.store_or_url('target', target, self._assert))
50
        node = new_ele("edit-config")
51
        node.append(util.datastore_or_url("target", target, self._assert))
57 52
        if error_option is not None:
58
            if error_option == 'rollback-on-error':
59
                self._assert(':rollback-on-error')
60
            subtree.append({
61
                'tag': 'error-option',
62
                'text': error_option
63
                })
53
            if error_option == "rollback-on-error":
54
                self._assert(":rollback-on-error")
55
            sub_ele(node, "error-option").text = error_option
64 56
        if test_option is not None:
65 57
            self._assert(':validate')
66
            subtree.append({
67
                'tag': 'test-option',
68
                'text': test_option
69
                })
58
            sub_ele(node, "test-option").text = test_option
70 59
        if default_operation is not None:
71
            subtree.append({
72
                'tag': 'default-operation',
73
                'text': default_operation
74
                })
75
        subtree.append(xml_.validated_element(config, ('config', xml_.qualify('config'))))
60
            # TODO: check if it is a valid default-operation
61
            sub_ele(node, "default-operation").text = default_operation
62
        node.append(validated_element(config, ("config", qualify("config"))))
76 63
        return self._request(spec)
77 64

  
65

  
78 66
class DeleteConfig(RPC):
79 67

  
80 68
    "*<delete-config>* RPC"
81 69

  
82
    SPEC = {'tag': 'delete-config', 'subtree': []}
83

  
84 70
    def request(self, target):
85 71
        """
86 72
        :arg target: See :ref:`source_target`
......
88 74

  
89 75
        :seealso: :ref:`return`
90 76
        """
91
        spec = deepcopy(DeleteConfig.SPEC)
92
        spec['subtree'].append(util.store_or_url('target', target, self._assert))
77
        node = new_ele("delete-config")
78
        node.append(util.datastore_or_url("target", target, self._assert))
93 79
        return self._request(spec)
94 80

  
95 81

  
96 82
class CopyConfig(RPC):
97 83

  
98 84
    "*<copy-config>* RPC"
99

  
100
    SPEC = {'tag': 'copy-config', 'subtree': []}
101

  
85
    
102 86
    def request(self, source, target):
103 87
        """
104 88
        :arg source: See :ref:`source_target`
......
109 93

  
110 94
        :seealso: :ref:`return`
111 95
        """
112
        spec = deepcopy(CopyConfig.SPEC)
113
        spec['subtree'].append(util.store_or_url('target', target, self._assert))
114
        spec['subtree'].append(util.store_or_url('source', source, self._assert))
96
        node = new_ele("copy-config")
97
        node.append(util.datastore_or_url("target", target, self._assert))
98
        node.append(util.datastore_or_url("source", source, self._assert))
115 99
        return self._request(spec)
116 100

  
117 101

  
......
121 105

  
122 106
    DEPENDS = [':validate']
123 107

  
124
    SPEC = {'tag': 'validate', 'subtree': []}
125

  
126 108
    def request(self, source):
127 109
        """
128 110
        :arg source: See :ref:`source_target`
......
130 112

  
131 113
        :seealso: :ref:`return`
132 114
        """
133
        spec = deepcopy(Validate.SPEC)
115
        node = new_ele("validate")
134 116
        try:
135
            src = xml_.validated_element(source, ('config', xml_.qualify('config')))
117
            src = validated_element(source, ("config", qualify("config")))
136 118
        except Exception as e:
137 119
            logger.debug(e)
138
            src = util.store_or_url('source', source, self._assert)
139
        spec['subtree'].append({
140
            'tag': 'source',
141
            'subtree': src
142
            })
120
            src = util.datastore_or_url("source", source, self._assert)
121
        (node if src.tag == "source" else sub_ele(node, "source")).append(src)
143 122
        return self._request(spec)
144 123

  
145 124

  
......
148 127
    "*<commit>* RPC. Depends on the *:candidate* capability."
149 128

  
150 129
    DEPENDS = [':candidate']
151

  
152
    SPEC = {'tag': 'commit', 'subtree': []}
153

  
154
    def _parse_hook(self):
155
        pass
156

  
130
    
157 131
    def request(self, confirmed=False, timeout=None):
158 132
        """
159 133
        Requires *:confirmed-commit* capability if *confirmed* argument is
......
167 141

  
168 142
        :seealso: :ref:`return`
169 143
        """
170
        spec = deepcopy(Commit.SPEC)
144
        node = new_ele("commit")
171 145
        if confirmed:
172
            self._assert(':confirmed-commit')
173
            spec['subtree'].append({'tag': 'confirmed'})
146
            self._assert(":confirmed-commit")
147
            sub_ele(node, "confirmed")
174 148
            if timeout is not None:
175
                spec['subtree'].append({
176
                    'tag': 'confirm-timeout',
177
                    'text': timeout
178
                })
149
                # TODO check if timeout is a valid integer?
150
                sub_ele(node, "confirm-timeout").text = timeout
179 151
        return self._request(Commit.SPEC)
180 152

  
181 153

  
182 154
class DiscardChanges(RPC):
183 155

  
184
    # TESTED
185

  
186 156
    "*<discard-changes>* RPC. Depends on the *:candidate* capability."
187 157

  
188
    DEPENDS = [':candidate']
189

  
190
    SPEC = {'tag': 'discard-changes'}
158
    DEPENDS = [":candidate"]
191 159

  
192 160
    def request(self):
193 161
        ":seealso: :ref:`return`"
194
        return self._request(DiscardChanges.SPEC)
162
        return self._request(new_ele("discard-changes"))
b/ncclient/operations/flowmon.py
14 14

  
15 15
'Power-control operations'
16 16

  
17
from ncclient import xml_
17
from ncclient.xml_ import *
18 18

  
19 19
from rpc import RPC
20 20

  
21
PC_URN = "urn:liberouter:params:xml:ns:netconf:power-control:1.0"
22

  
21 23
class PoweroffMachine(RPC):
22 24

  
23 25
    "*poweroff-machine* RPC (flowmon)"
24 26

  
25
    DEPENDS = [':power-control:1.0']
26

  
27
    SPEC = {'tag': xml_.qualify('poweroff-machine',
28
                                'urn:liberouter:params:xml:ns:netconf:power-control:1.0')}
27
    DEPENDS = ["urn:liberouter:params:netconf:capability:power-control:1.0"]
28
    
29
    def request(self, target):
30
        return self._request(new_ele(qualify("poweroff-machine", PC_URN)))
29 31

  
30 32
class RebootMachine(RPC):
31 33

  
32 34
    "*reboot-machine* RPC (flowmon)"
33 35

  
34
    DEPENDS = [':power-control:1.0']
36
    DEPENDS = ["urn:liberouter:params:netconf:capability:power-control:1.0"]
35 37

  
36
    SPEC = {'tag': xml_.qualify('reboot-machine',
37
                                'urn:liberouter:params:xml:ns:netconf:power-control:1.0')}
38
    def request(self, target):
39
        return self._request(new_ele(qualify("reboot-machine", PC_URN)))
b/ncclient/operations/lock.py
14 14

  
15 15
'Locking-related NETCONF operations'
16 16

  
17
from copy import deepcopy
17
from ncclient.xml_ import *
18 18

  
19 19
from rpc import RPC
20 20

  
21 21
# TODO:
22
# should have some way to parse session-id from a lock-denied error
22
# should have some way to parse session-id from a lock-denied error, and raise
23
# a tailored exception
23 24

  
24 25
class Lock(RPC):
25 26

  
26 27
    "*<lock>* RPC"
27

  
28
    SPEC = {
29
        'tag': 'lock',
30
        'subtree': {
31
            'tag': 'target',
32
            'subtree': {'tag': None }
33
        }
34
    }
35

  
36
    #REPLY_CLS = LockReply
37

  
28
    
38 29
    def request(self, target):
39 30
        """
40 31
        :arg target: see :ref:`source_target`
......
42 33

  
43 34
        :rtype: :ref:`return`
44 35
        """
45
        spec = deepcopy(Lock.SPEC)
46
        spec['subtree']['subtree']['tag'] = target
47
        return self._request(spec)
36
        node = new_ele("lock")
37
        sub_ele(sub_ele(node, "target"), "running")
38
        return self._request(node)
48 39

  
49 40

  
50 41
class Unlock(RPC):
51 42

  
52 43
    "*<unlock>* RPC"
53

  
54
    SPEC = {
55
        'tag': 'unlock',
56
        'subtree': {
57
            'tag': 'target',
58
            'subtree': {'tag': None }
59
        }
60
    }
61

  
44
    
62 45
    def request(self, target):
63 46
        """
64 47
        :arg target: see :ref:`source_target`
......
66 49

  
67 50
        :rtype: :ref:`return`
68 51
        """
69
        spec = deepcopy(Unlock.SPEC)
70
        spec['subtree']['subtree']['tag'] = target
71
        return self._request(spec)
52
        node = new_ele("lock")
53
        sub_ele(sub_ele(node, "target"), "running")
54
        return self._request(node)
72 55

  
73 56

  
74 57
class LockContext:
......
87 70
        self.target = target
88 71

  
89 72
    def __enter__(self):
90
        reply = Lock(self.session).request(self.target)
91
        if not reply.ok: # an error locking should definitely always be raised
92
            raise reply.error
93
        else:
94
            return self
73
        Lock(self.session).request(self.target)
74
        return self
95 75

  
96 76
    def __exit__(self, *args):
97
        reply = Unlock(self.session).request(self.target)
98
        if not reply.ok:
99
            raise reply.error
77
        Unlock(self.session).request(self.target)
100 78
        return False
b/ncclient/operations/retrieve.py
14 14

  
15 15
from rpc import RPC, RPCReply
16 16

  
17
from ncclient import xml_
18
from copy import deepcopy
17
from ncclient.xml_ import *
19 18

  
20 19
import util
21 20

  
......
27 26
    def _parsing_hook(self, root):
28 27
        self._data = None
29 28
        if not self._errors:
30
            self._data = xml_.find(root, 'data', nslist=xml_.NSLIST)
29
            self._data = root.find(qualify("data"))
31 30

  
32 31
    @property
33 32
    def data_ele(self):
......
41 40
        "*<data>* element as an XML string"
42 41
        if not self._parsed:
43 42
            self.parse()
44
        return xml_.ele2xml(self._data)
45

  
46
    @property
47
    def data_dtree(self):
48
        "*<data>* element in :ref:`dtree`"
49
        return xml_.ele2dtree(self._data)
50

  
43
        return to_xml(self._data)
44
    
51 45
    #: Same as :attr:`data_ele`
52 46
    data = data_ele
47
    
48
    #def __repr__(self):
49
    #    return self.data_xml
53 50

  
54 51

  
55 52
class Get(RPC):
56 53

  
57 54
    "The *<get>* RPC"
58 55

  
59
    SPEC = {'tag': 'get', 'subtree': []}
60

  
61 56
    REPLY_CLS = GetReply
62 57

  
63 58
    def request(self, filter=None):
......
66 61

  
67 62
        :seealso: :ref:`return`
68 63
        """
69
        spec = deepcopy(Get.SPEC)
64
        node = new_ele("get")
70 65
        if filter is not None:
71
            spec['subtree'].append(util.build_filter(filter))
72
        return self._request(spec)
66
            node.append(util.build_filter(filter))
67
        return self._request(node)
73 68

  
74 69

  
75 70
class GetConfig(RPC):
76 71

  
77 72
    "The *<get-config>* RPC"
78 73

  
79
    SPEC = {'tag': 'get-config', 'subtree': []}
80

  
81 74
    REPLY_CLS = GetReply
82 75

  
83 76
    def request(self, source, filter=None):
......
88 81

  
89 82
        :seealso: :ref:`return`
90 83
        """
91
        spec = deepcopy(GetConfig.SPEC)
92
        spec['subtree'].append(util.store_or_url('source', source, self._assert))
84
        node = new_ele("get-config")
85
        node.append(util.datastore_or_url("source", source, self._assert))
93 86
        if filter is not None:
94
            spec['subtree'].append(util.build_filter(filter))
95
        return self._request(spec)
87
            node.append(util.build_filter(filter))
88
        return self._request(node)
b/ncclient/operations/rpc.py
15 15
from threading import Event, Lock
16 16
from uuid import uuid1
17 17

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

  
21 21
from errors import OperationError, TimeoutExpiredError, MissingCapabilityError
22 22

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

  
26 26

  
27 27
class RPCReply:
......
55 55
        """Parse the *<rpc-reply>*"""
56 56
        if self._parsed:
57 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_.NSLIST)
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_.NSLIST)
58
        root = self._root = to_ele(self._raw) # The <rpc-reply> element
59
        # Per RFC 4741 an <ok/> tag is sent when there are no errors or warnings
60
        ok = root.find(qualify("ok"))
61
        if ok is None:
62
            # Create RPCError objects from <rpc-error> elements
63
            error = root.find(qualify("rpc-error"))
65 64
            if error is not None:
66
                logger.debug('parsed [%s]' % error.tag)
67 65
                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))
66
                    # Process a particular <rpc-error>
67
                    self._errors.append(RPCError(err))
77 68
        self._parsing_hook(root)
78 69
        self._parsed = True
79 70

  
......
110 101
        return self._errors
111 102

  
112 103

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

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

  
118
    def __init__(self, err_dict):
119
        self._dict = err_dict
109
    def __init__(self, err):
110
        self._type = None
111
        self._severity = None
112
        self._info = None
113
        self._tag = None
114
        self._path = None
115
        self._message = None
116
        for subele in err:
117
            if subele.tag == qualify("error-tag"):
118
                self._tag = subele.text
119
            elif subele.tag == qualify("error-severity"):
120
                self._severity = subele.text
121
            elif subele.tag == qualify("error-info"):
122
                self._info = subele.text
123
            elif subele.tag == qualify("error-path"):
124
                self._path = subele.text
125
            elif subele.tag == qualify("error-message"):
126
                self._message = subele.text
120 127
        if self.message is not None:
121 128
            OperationError.__init__(self, self.message)
122 129
        else:
......
125 132
    @property
126 133
    def type(self):
127 134
        "`string` representing text of *error-type* element"
128
        return self.get('error-type', None)
135
        return self._type
129 136

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

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

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

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

  
150 157
    @property
151 158
    def info(self):
152 159
        "`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)
160
        return self._info
178 161

  
179 162

  
180 163
class RPCReplyListener(SessionListener):
......
198 181

  
199 182
    def callback(self, root, raw):
200 183
        tag, attrs = root
201
        if xml_.unqualify(tag) != 'rpc-reply':
184
        if tag != qualify("rpc-reply"):
202 185
            return
203 186
        for key in attrs: # in the <rpc-reply> attributes
204
            if xml_.unqualify(key) == 'message-id': # if we found msgid attr
187
            logger.debug("key=%s" % key)
188
            if key == "message-id": # if we found msgid attr
205 189
                id = attrs[key] # get the msgid
206
                try:
207
                    with self._lock:
208
                        rpc = self._id2rpc.get(id) # the corresponding rpc
209
                        logger.debug('delivering to %r' % rpc)
190
                with self._lock:
191
                    try:                    
192
                        rpc = self._id2rpc[id] # the corresponding rpc
193
                        logger.debug("Delivering to %r" % rpc)
210 194
                        rpc.deliver_reply(raw)
211
                except KeyError:
212
                    raise OperationError('Unknown message-id: %s', id)
213
                # no catching other exceptions, fail loudly if must
214
                else:
215
                    # if no error delivering, can del the reference to the RPC
216
                    del self._id2rpc[id]
217
                    break
195
                    except KeyError:
196
                        raise OperationError("Unknown message-id: %s", id)
197
                    # no catching other exceptions, fail loudly if must
198
                    else:
199
                        # if no error delivering, can del the reference to the RPC
200
                        del self._id2rpc[id]
201
                        break
218 202
        else:
219
            raise OperationError('Could not find "message-id" attribute in <rpc-reply>')
203
            raise OperationError("Could not find 'message-id' attribute in <rpc-reply>")
220 204
    
221 205
    def errback(self, err):
222 206
        try:
......
244 228
    # subclass of :class:`RPCReply`.
245 229
    REPLY_CLS = RPCReply
246 230

  
247
    def __init__(self, session, async=False, timeout=None, raise_mode='none'):
231
    def __init__(self, session, async=False, timeout=None, raise_mode="none"):
248 232
        self._session = session
249 233
        try:
250 234
            for cap in self.DEPENDS:
......
254 238
        self._async = async
255 239
        self._timeout = timeout
256 240
        self._raise_mode = raise_mode
257
        # keeps things simple instead of having a class attr that has to be locked
258
        self._id = uuid1().urn
241
        self._id = uuid1().urn # Keeps things simple instead of having a class attr that has to be locked
259 242
        self._listener = RPCReplyListener(session)
260 243
        self._listener.register(self._id, self)
261 244
        self._reply = None
262 245
        self._error = None
263 246
        self._event = Event()
264 247

  
265
    def _build(self, opspec):
248
    def _build(self, subele):
266 249
        # internal
267
        spec = {
268
            'tag': 'rpc',
269
            'attrib': {
270
                'xmlns': xml_.BASE_NS_1_0,
271
                'message-id': self._id
272
                },
273
            'subtree': [ opspec ]
274
            }
275
        return xml_.dtree2xml(spec)
250
        ele = new_ele("rpc", {"message-id": self._id}, xmlns=BASE_NS_1_0)
251
        ele.append(subele)
252
        return to_xml(ele)
276 253

  
277 254
    def _request(self, op):
278 255
        """Subclasses call this method to make the RPC request.
......
288 265
        :type opspec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
289 266
        :rtype: :class:`RPCReply` (sync) or :class:`RPC` (async)
290 267
        """
291
        logger.debug('request %r with opsepc=%r' % (self, op))
268
        logger.info('Requesting %r' % self.__class__.__name__)
292 269
        req = self._build(op)
293 270
        self._session.send(req)
294 271
        if self._async:
295
            logger.debug('async, returning')
272
            logger.debug('Async request, returning %r', self)
296 273
            return self
297 274
        else:
298
            logger.debug('sync, will wait for timeout=%r' % self._timeout)
275
            logger.debug('Sync request, will wait for timeout=%r' %
276
                         self._timeout)
299 277
            self._event.wait(self._timeout)
300 278
            if self._event.isSet():
301 279
                if self._error:
......
330 308
    def deliver_reply(self, raw):
331 309
        # internal use
332 310
        self._reply = self.REPLY_CLS(raw)
333
        #self._delivery_hook() -- usecase?!
334 311
        self._event.set()
335 312

  
336 313
    def deliver_error(self, err):
337 314
        # internal use
338 315
        self._error = err
339
        #self._delivery_hook() -- usecase?!
340 316
        self._event.set()
341 317

  
342 318
    @property
......
379 355
            raise UserWarning('Asynchronous mode not supported for this device/session')
380 356

  
381 357
    def set_raise_mode(self, mode):
382
        assert(choice in ('all', 'errors', 'none'))
358
        assert(choice in ("all", "errors", "none"))
383 359
        self._raise_mode = mode
384 360

  
385 361
    def set_timeout(self, timeout):
386
        """Set the timeout for synchronous waiting defining how long the RPC
387
        request will block on a reply before raising an error."""
362
        """Set the timeout for synchronous waiting; defining how long the RPC
363
        request will block on a reply before raising an error. Irrelevant for
364
        asynchronous usage."""
388 365
        self._timeout = timeout
389 366

  
390 367
    #: Whether this RPC is asynchronous
391
    async = property(fget=lambda self: self._async, fset=set_async)
368
    is_async = property(fget=lambda self: self._async, fset=set_async)
392 369

  
393 370
    #: Timeout for synchronous waiting
394 371
    timeout = property(fget=lambda self: self._timeout, fset=set_timeout)
b/ncclient/operations/session.py
14 14

  
15 15
'Session-related NETCONF operations'
16 16

  
17
from copy import deepcopy
17
from ncclient.xml_ import *
18 18

  
19 19
from rpc import RPC
20 20

  
......
22 22

  
23 23
    "*<close-session>* RPC. The connection to NETCONF server is also closed."
24 24

  
25
    SPEC = { 'tag': 'close-session' }
26

  
27 25
    def request(self):
28 26
        try:
29
            return self._request(CloseSession.SPEC)
27
            return self._request(new_ele("close-sesion"))
30 28
        finally:
31 29
            self.session.close()
32 30

  
......
34 32
class KillSession(RPC):
35 33

  
36 34
    "*<kill-session>* RPC."
37

  
38
    SPEC = {
39
        'tag': 'kill-session',
40
        'subtree': []
41
    }
42

  
35
    
43 36
    def request(self, session_id):
44 37
        """
45 38
        :arg session_id: *session-id* of NETCONF session to kill
......
47 40

  
48 41
        :seealso: :ref:`return`
49 42
        """
50
        spec = deepcopy(KillSession.SPEC)
43
        node = new_ele("kill-session")
51 44
        if not isinstance(session_id, basestring): # make sure
52 45
            session_id = str(session_id)
53
        spec['subtree'].append({
54
            'tag': 'session-id',
55
            'text': session_id
56
        })
57
        return self._request(spec)
46
        sub_ele(node, "session-id").text = session_id
47
        return self._request(node)
b/ncclient/operations/subscribe.py
12 12
# See the License for the specific language governing permissions and
13 13
# limitations under the License.
14 14

  
15
# TODO
16

  
15 17
class Notification:
16 18
    pass
17 19

  
b/ncclient/operations/util.py
14 14

  
15 15
'Boilerplate ugliness'
16 16

  
17
from ncclient import xml_
17
from ncclient.xml_ import *
18 18

  
19 19
from errors import OperationError, MissingCapabilityError
20 20

  
21 21
def one_of(*args):
22
    'Verifies that only one of the arguments is not None'
22
    "Verifies that only one of the arguments is not None"
23 23
    for i, arg in enumerate(args):
24 24
        if arg is not None:
25 25
            for argh in args[i+1:]:
26 26
                if argh is not None:
27
                    raise OperationError('Too many parameters')
27
                    raise OperationError("Too many parameters")
28 28
            else:
29 29
                return
30
    raise OperationError('Insufficient parameters')
30
    raise OperationError("Insufficient parameters")
31 31

  
32
def store_or_url(wha, loc, capcheck=None):
33
    node = { 'tag': wha, 'subtree': {} }
34
    if '://' in loc: # e.g. http://, file://, ftp://
32
def datastore_or_url(wha, loc, capcheck=None):
33
    node = new_ele(wha)
34
    if "://" in loc: # e.g. http://, file://, ftp://
35 35
        if capcheck is not None:
36
            capcheck(':url') # url schema check at some point!
37
        node['subtree']['tag'] = 'url'
38
        node['subtree']['text'] = loc
36
            capcheck(":url") # url schema check at some point!
37
            sub_ele(node, "url").text = loc
39 38
    else:
40 39
        #if loc == 'candidate':
41 40
        #    capcheck(':candidate')
......
43 42
        #    capcheck(':startup')
44 43
        #elif loc == 'running' and wha == 'target':
45 44
        #    capcheck(':writable-running')
46
        node['subtree']['tag'] = loc
45
        sub_ele(node, loc)
47 46
    return node
48 47

  
49 48
def build_filter(spec, capcheck=None):
50 49
    type = None
51 50
    if isinstance(spec, tuple):
52 51
        type, criteria = spec
53
        rep = {'tag': 'filter', 'attrib': {'type': type}}
54
        if type == 'xpath':
55
            rep['attrib']['select'] = criteria
56
        elif type == 'subtree':
57
            rep['subtree'] = criteria
52
        rep = new_ele("filter", type=type)
53
        if type == "xpath":
54
            rep.attrib["select"] = criteria
55
        elif type == "subtree":
56
            rep.append(to_ele(criteria))
58 57
        else:
59 58
            raise OperationError("Invalid filter type")
60 59
    else:
61
        rep = xml_.validated_element(spec, ['filter', xml_.qualify('filter')],
62
                                        attrs=[('type', xml_.qualify('type'))])
63
    if type == 'xpath' and capcheck is not None:
64
        capcheck(':xpath')
60
        rep = validated_element(spec, ("filter", qualify("filter")),
61
                                        attrs=("type",))
62
        # TODO set type var here, check if select attr present in case of xpath..
63
    if type == "xpath" and capcheck is not None:
64
        capcheck(":xpath")
65 65
    return rep
b/ncclient/transport/errors.py
25 25
    def __init__(self, in_buf, out_buf=None):
26 26
        msg = 'Unexpected session close'
27 27
        if in_buf:
28
            msg += '\nIN_BUFFER: {%s}' % in_buf
28
            msg += '\nIN_BUFFER: `%s`' % in_buf
29 29
        if out_buf:
30
            msg += ' OUT_BUFFER: {%s}' % out_buf
30
            msg += ' OUT_BUFFER: `%s`' % out_buf
31 31
        SSHError.__init__(self, msg)
32 32

  
33 33
class SSHError(TransportError):
b/ncclient/transport/session.py
15 15
from Queue import Queue
16 16
from threading import Thread, Lock, Event
17 17

  
18
from ncclient import xml_
18
from ncclient.xml_ import *
19 19
from ncclient.capabilities import Capabilities
20 20

  
21 21
from errors import TransportError
......
30 30
    def __init__(self, capabilities):
31 31
        Thread.__init__(self)
32 32
        self.setDaemon(True)
33
        self._listeners = set() # 3.0's weakset would be ideal
33
        self._listeners = set()
34 34
        self._lock = Lock()
35 35
        self.setName('session')
36 36
        self._q = Queue()
......
43 43

  
44 44
    def _dispatch_message(self, raw):
45 45
        try:
46
            root = xml_.parse_root(raw)
46
            root = parse_root(raw)
47 47
        except Exception as e:
48 48
            logger.error('error parsing dispatch message: %s' % e)
49 49
            return
......
203 203
        self._error_cb = error_cb
204 204

  
205 205
    def callback(self, root, raw):
206
        if xml_.unqualify(root[0]) == 'hello':
206
        tag, attrs = root
207
        if tag == qualify("hello"):
207 208
            try:
208 209
                id, capabilities = HelloHandler.parse(raw)
209 210
            except Exception as e:
......
217 218
    @staticmethod
218 219
    def build(capabilities):
219 220
        "Given a list of capability URI's returns <hello> message XML string"
220
        spec = {
221
            'tag': 'hello',
222
            'attrib': {'xmlns': xml_.BASE_NS_1_0},
223
            'subtree': [{
224
                'tag': 'capabilities',
225
                'subtree': # this is fun :-)
226
                    [{'tag': 'capability', 'text': uri} for uri in capabilities]
227
                }]
228
            }
229
        return xml_.dtree2xml(spec)
221
        hello = new_ele("hello", xmlns=BASE_NS_1_0)
222
        caps = sub_ele(hello, "capabilities")
223
        def fun(uri): sub_ele(caps, "capability").text = uri
224
        map(fun, capabilities)
225
        return to_xml(hello)
230 226

  
231 227
    @staticmethod
232 228
    def parse(raw):
233 229
        "Returns tuple of (session-id (str), capabilities (Capabilities)"
234 230
        sid, capabilities = 0, []
235
        root = xml_.xml2ele(raw)
231
        root = to_ele(raw)
236 232
        for child in root.getchildren():
237
            tag = xml_.unqualify(child.tag)
238
            if tag == 'session-id':
233
            if child.tag == qualify("session-id"):
239 234
                sid = child.text
240
            elif tag == 'capabilities':
235
            elif child.tag == qualify("capabilities"):
241 236
                for cap in child.getchildren():
242
                    if xml_.unqualify(cap.tag) == 'capability':
237
                    if cap.tag == qualify("capability"):
243 238
                        capabilities.append(cap.text)
244 239
        return sid, Capabilities(capabilities)
b/ncclient/xml_.py
23 23

  
24 24
from ncclient import NCClientError
25 25

  
26
class ContentError(NCClientError):
27
    "Raised by methods of the :mod:`content` module in case of an error."
26
class XMLError(NCClientError):
28 27
    pass
29 28

  
30 29
### Namespace-related
31 30

  
32 31
#: Base NETCONF namespace
33 32
BASE_NS_1_0 = 'urn:ietf:params:xml:ns:netconf:base:1.0'
34
#: ... and this is BASE_NS according to Cisco devices tested
35
CISCO_BS_1_0 = 'urn:ietf:params:netconf:base:1.0'
36 33
#: namespace for Tail-f data model
37 34
TAILF_AAA_1_1 = 'http://tail-f.com/ns/aaa/1.1'
38 35
#: namespace for Tail-f data model
......
63 60

  
64 61
qualify = lambda tag, ns=BASE_NS_1_0: tag if ns is None else '{%s}%s' % (ns, tag)
65 62

  
66
multiqualify = lambda tag, nslist=(BASE_NS_1_0, CISCO_BS_1_0): [qualify(tag, ns) for ns in nslist]
67

  
68
unqualify = lambda tag: tag[tag.rfind('}')+1:]
69

  
70
### XML representations
71

  
72
class DictTree:
73

  
74
    @staticmethod
75
    def Element(spec):
76
        """DictTree -> Element
77

  
78
        :type spec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
79

  
80
        :rtype: :class:`~xml.etree.ElementTree.Element`
81
        """
82
        if iselement(spec):
83
            return spec
84
        elif isinstance(spec, basestring):
85
            return XML.Element(spec)
86
        if not isinstance(spec, dict):
87
            raise ContentError("Invalid tree spec")
88
        if 'tag' in spec:
89
            ele = ET.Element(spec.get('tag'), spec.get('attrib', {}))
90
            ele.text = spec.get('text', '')
91
            ele.tail = spec.get('tail', '')
92
            subtree = spec.get('subtree', [])
93
            # might not be properly specified as list but may be dict
94
            if not isinstance(subtree, list):
95
                subtree = [subtree]
96
            for subele in subtree:
97
                ele.append(DictTree.Element(subele))
98
            return ele
99
        elif 'comment' in spec:
100
            return ET.Comment(spec.get('comment'))
101
        else:
102
            raise ContentError('Invalid tree spec')
103

  
104
    @staticmethod
105
    def XML(spec, encoding='UTF-8'):
106
        """DictTree -> XML
107

  
108
        :type spec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
109

  
110
        :arg encoding: chraracter encoding
111

  
112
        :rtype: string
113
        """
114
        return Element.XML(DictTree.Element(spec), encoding)
115

  
116
class Element:
117

  
118
    @staticmethod
119
    def DictTree(ele):
120
        """DictTree -> Element
121

  
122
        :type spec: :class:`~xml.etree.ElementTree.Element`
123
        :rtype: :obj:`dict`
124
        """
125
        return {
126
            'tag': ele.tag,
127
            'attributes': ele.attrib,
128
            'text': ele.text,
129
            'tail': ele.tail,
130
            'subtree': [ Element.DictTree(child) for child in ele.getchildren() ]
131
        }
132

  
133
    @staticmethod
134
    def XML(ele, encoding='UTF-8'):
135
        """Element -> XML
136

  
137
        :type spec: :class:`~xml.etree.ElementTree.Element`
138
        :arg encoding: character encoding
139
        :rtype: :obj:`string`
140
        """
141
        xml = ET.tostring(ele, encoding)
142
        if xml.startswith('<?xml'):
143
            return xml
144
        else:
145
            return '<?xml version="1.0" encoding="%s"?>%s' % (encoding, xml)
146

  
147
class XML:
148

  
149
    @staticmethod
150
    def DictTree(xml):
151
        """XML -> DictTree
152

  
153
        :type spec: :obj:`string`
154
        :rtype: :obj:`dict`
155
        """
156
        return Element.DictTree(XML.Element(xml))
157

  
158
    @staticmethod
159
    def Element(xml):
160
        """XML -> Element
161

  
162
        :type xml: :obj:`string`
163
        :rtype: :class:`~xml.etree.ElementTree.Element`
164
        """
165
        return ET.fromstring(xml)
166

  
167
dtree2ele = DictTree.Element
168
dtree2xml = DictTree.XML
169
ele2dtree = Element.DictTree
170
ele2xml = Element.XML
171
xml2dtree = XML.DictTree
172
xml2ele = XML.Element
173

  
174
### Other utility functions
63
#unqualify = lambda tag: tag[tag.rfind('}')+1:]
175 64

  
176
iselement = ET.iselement
177

  
178

  
179
NSLIST = [BASE_NS_1_0, CISCO_BS_1_0]
180

  
181
def find(ele, tag, nslist=[]):
182
    """If *nslist* is empty, same as :meth:`xml.etree.ElementTree.Element.find`.
183
    If it is not, *tag* is interpreted as an unqualified name and qualified
184
    using each item in *nslist* (with a :const:`None` item in *nslit* meaning no
185
    qualification is done). The first match is returned.
186

  
187
    :arg nslist: optional list of namespaces
188
    :type nslit: `string` `list`
65
def to_xml(ele, encoding="UTF-8"):
66
    """Element -> XML
67
    
68
    :type spec: :class:`~xml.etree.ElementTree.Element`
69
    :arg encoding: character encoding
70
    :rtype: :obj:`string`
189 71
    """
190
    if nslist:
191
        for qname in multiqualify(tag):
192
            found = ele.find(qname)
193
            if found is not None:
194
                return found
195
    else:
196
        return ele.find(tag)
72
    xml = ET.tostring(ele, encoding)
73
    return xml if xml.startswith('<?xml') else '<?xml version="1.0" encoding="%s"?>%s' % (encoding, xml)
74

  
75
def to_ele(x):
76
    """XML -> Element
77
    
78
    :type xml: :obj:`string`
79
    :rtype: :class:`~xml.etree.ElementTree.Element`
80
    """
81
    return x if iselement(x) else ET.fromstring(x)
82

  
83
iselement = ET.iselement
197 84

  
198 85
def parse_root(raw):
199 86
    """Efficiently parses the root element of an XML document.
......
203 90
    :returns: a tuple of `(tag, attributes)`, where `tag` is the (qualified) name of the element and `attributes` is a dictionary of its attributes.
204 91
    :rtype: `tuple`
205 92
    """
206
    fp = StringIO(raw[:1024]) # this is a guess but start element beyond 1024 bytes would be a bit absurd
93
    fp = StringIO(raw)
207 94
    for event, element in ET.iterparse(fp, events=('start',)):
208 95
        return (element.tag, element.attrib)
209 96

  
210
def validated_element(rep, tags=None, attrs=None, text=None):
97
def validated_element(x, tags=None, attrs=None):
211 98
    """Checks if the root element meets the supplied criteria. Returns a
212 99
    :class:`~xml.etree.ElementTree.Element` instance if so, otherwise raises
213 100
    :exc:`ContentError`.
214 101

  
215
    :arg tags: tag name or a list of allowable tag names
216
    :arg attrs: list of required attribute names, each item may be a list of allowable alternatives
102
    :arg tags: tag name or a sequence of allowable tag names
103
    :arg attrs: sequence of required attribute names, each item may be a list of allowable alternatives
217 104
    :arg text: textual content to match
218
    :type rep: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
105
    :type rep: :class:`~xml.etree.ElementTree.Element`
219 106
    """
220
    ele = dtree2ele(rep)
221
    err = False
107
    ele = to_ele(x)
222 108
    if tags:
223 109
        if isinstance(tags, basestring):
224 110
            tags = [tags]
225 111
        if ele.tag not in tags:
226
            err = True
112
            raise XMLError("Element [%s] does not meet requirement" % ele.tag)
227 113
    if attrs:
228 114
        for req in attrs:
229 115
            if isinstance(req, basestring): req = [req]
......
231 117
                if alt in ele.attrib:
232 118
                    break
233 119
            else:
234
                err = True
235
    if text and ele.text != text:
236
        err = True
237
    if err:
238
        raise ContentError("Element [%s] does not meet requirements" % ele.tag)
120
                raise XMLError("Element [%s] does not have required attributes" % ele.tag)
239 121
    return ele
122

  
123
def new_ele(tag, attrs={}, **extra):
124
    return ET.Element(tag, attrs, **extra)
125

  
126
def sub_ele(parent, tag, attrs={}, **extra):
127
    return ET.SubElement(parent, tag, attrs, **extra)

Also available in: Unified diff