Revision a7cb58ce

b/ncclient/capabilities.py
14 14

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

  
19
def schemes(uri):
20
    return uri.partition("?scheme=")[2].split(',')
21
def version(uri):
22
    if uri.startswith('urn:ietf:params:netconf:capability:'):
23
        return uri.split(':')[6]
24
    elif uri.startswith('urn:ietf:params:netconf:base:'):
25
        return uri.split(':')[5]
21 26

  
22 27
class Capabilities:
23
    
24
    """Represent the capabilities of client or server. Also facilitates using
25
    abbreviated capability names in addition to complete URI.
26
    """
27
    
28

  
28 29
    def __init__(self, capabilities=None):
29 30
        self._dict = {}
30 31
        if isinstance(capabilities, dict):
31 32
            self._dict = capabilities
32 33
        elif isinstance(capabilities, list):
33 34
            for uri in capabilities:
34
                self._dict[uri] = abbreviate(uri)
35
    
35
                self._dict[uri] = (abbreviate(uri), version(uri))
36

  
36 37
    def __contains__(self, key):
37
        return ( key in self._dict ) or ( key in self._dict.values() )
38
    
38
        if key in self._dict:
39
            return True
40
        for info in self._dict.values():
41
            if key == info[0]:
42
                return True
43
        return False
44

  
39 45
    def __iter__(self):
40 46
        return self._dict.keys().__iter__()
41
    
47

  
42 48
    def __repr__(self):
43 49
        return repr(self._dict.keys())
44
    
50

  
45 51
    def __list__(self):
46 52
        return self._dict.keys()
47
    
48
    def add(self, uri, shorthand=None):
49
        if shorthand is None:
50
            shorthand = abbreviate(uri)
51
        self._dict[uri] = shorthand
52
    
53

  
54
    def add(self, uri, info=None):
55
        if info is None:
56
            info = (abbreviate(uri), version(uri))
57
        self._dict[uri] = info
58

  
53 59
    set = add
54
    
60

  
55 61
    def remove(self, key):
56 62
        if key in self._dict:
57 63
            del self._dict[key]
58 64
        else:
59 65
            for uri in self._dict:
60
                if self._dict[uri] == key:
66
                if key in self._dict[uri]:
61 67
                    del self._dict[uri]
62 68
                    break
63 69

  
64
# : the capabilities currently supported by ncclient
70
    def get_uri(self, shortname):
71
        for uri, info in self._dict.items():
72
            if info[0] == shortname:
73
                return uri
74

  
75
    def url_schemes(self):
76
        url_uri = get_uri(':url')
77
        if url_uri is None:
78
            return []
79
        else:
80
            return url_uri.partition("?scheme=")[2].split(',')
81

  
82
    def version(self, key):
83
        try:
84
            return self._dict[key][1]
85
        except KeyError:
86
            for uri, info in self._dict.items():
87
                if info[0] == key:
88
                    return info[1]
89

  
90

  
91
#: the capabilities supported by NCClient
65 92
CAPABILITIES = Capabilities([
66 93
    'urn:ietf:params:netconf:base:1.0',
67 94
    'urn:ietf:params:netconf:capability:writable-running:1.0',
......
74 101
    'urn:ietf:params:netconf:capability:xpath:1.0',
75 102
    #'urn:ietf:params:netconf:capability:notification:1.0', # TODO
76 103
    #'urn:ietf:params:netconf:capability:interleave:1.0' # theoretically already supported
77
])
104
])
b/ncclient/content.py
12 12
# See the License for the specific language governing permissions and
13 13
# limitations under the License.
14 14

  
15

  
16
"""The :mod:`content` module provides methods for creating XML documents, parsing XML, and converting between different XML representations. It uses :mod:`~xml.etree.ElementTree` internally.
17
"""
18

  
15 19
from cStringIO import StringIO
16 20
from xml.etree import cElementTree as ET
17 21

  
......
23 27

  
24 28
### Namespace-related
25 29

  
26
# : Base NETCONf namespace
30
#: Base NETCONf namespace
27 31
BASE_NS = 'urn:ietf:params:xml:ns:netconf:base:1.0'
28
# : ... and this is BASE_NS according to Cisco devices tested
32
#: ... and this is BASE_NS according to Cisco devices tested
29 33
CISCO_BS = 'urn:ietf:params:netconf:base:1.0'
30 34

  
31 35
try:
b/ncclient/manager.py
13 13
# limitations under the License.
14 14

  
15 15
import capabilities
16
import operations
16
from operations import OPERATIONS
17 17
import transport
18 18

  
19 19

  
......
25 25

  
26 26
connect = ssh_connect # default session type
27 27

  
28
RAISE_ALL, RAISE_ERROR, RAISE_NONE = range(3)
28
#: Raise all errors
29
RAISE_ALL = 0
30
#:
31
RAISE_ERR = 1
32
#:
33
RAISE_NONE = 2
29 34

  
30 35
class Manager:
31 36

  
......
33 38

  
34 39
    def __init__(self, session):
35 40
        self._session = session
36
        self._rpc_error_handling = RAISE_ALL
41
        self._rpc_error_action = RAISE_ALL
37 42

  
38
    def set_rpc_error_option(self, option):
43
    def set_rpc_error_action(self, action):
39 44
        self._rpc_error_handling = option
40 45

  
41 46
    def do(self, op, *args, **kwds):
42
        op = operations.OPERATIONS[op](self._session)
47
        op = OPERATIONS[op](self._session)
43 48
        reply = op.request(*args, **kwds)
44 49
        if not reply.ok:
45 50
            if self._raise == RAISE_ALL:
......
59 64

  
60 65
    def locked(self, target):
61 66
        """Returns a context manager for use with the 'with' statement.
62
        `target` is the datastore to lock, e.g. 'candidate
67

  
68
        :arg target: name of the datastore to lock
69
        :type target: `string`
63 70
        """
64 71
        return operations.LockContext(self._session, target)
65 72

  
66
    get = lambda self, *args, **kwds: self.do('get', *args, **kwds).data
73
    def get(self, filter=None):
74
        pass
75

  
76
    def get_config(self, source, filter=None):
77
        pass
78

  
79
    def copy_config(self, source, target):
80
        pass
81

  
82
    def validate(self, source):
83
        pass
84

  
85
    def commit(self, target):
86
        pass
87

  
88
    def discard_changes(self):
89
        pass
90

  
91
    def delete_config(self, target):
92
        pass
67 93

  
68
    get_config = lambda self, *args, **kwds: self.do('get-config', *args, **kwds).data
94
    def lock(self, target):
95
        pass
69 96

  
70
    edit_config = lambda self, *args, **kwds: self.do('edit-config', *args, **kwds)
97
    def unlock(self, target):
98
        pass
71 99

  
72
    copy_config = lambda self, *args, **kwds: self.do('copy-config', *args, **kwds)
100
    def close_session(self):
101
        pass
73 102

  
74
    validate = lambda self, *args, **kwds: self.do('validate', *args, **kwds)
103
    def kill_session(self, session_id):
104
        pass
75 105

  
76
    commit = lambda self, *args, **kwds: self.do('commit', *args, **kwds)
106
    def confirmed_commit(self, timeout=None):
107
        pass
77 108

  
78
    discard_changes = lambda self, *args, **kwds: self.do('discard-changes', *args, **kwds)
109
    def confirm(self):
110
        # give confirmation
111
        pass
79 112

  
80
    delete_config = lambda self, *args, **kwds: self.do('delete-config', *args, **kwds)
113
    def discard_changes(self):
114
        pass
81 115

  
82 116
    lock = lambda self, *args, **kwds: self.do('lock', *args, **kwds)
83 117

  
......
90 124
    def close(self):
91 125
        try: # try doing it clean
92 126
            self.close_session()
93
        except:
94
            pass
127
        except Exception as e:
128
            logger.debug('error doing <close-session> -- %r' % e)
95 129
        if self._session.connected: # if that didn't work...
96 130
            self._session.close()
97 131

  
......
99 133
    def session(self, session):
100 134
        return self._session
101 135

  
102
    def get_capabilities(self, whose):
103
        if whose in ('manager', 'client'):
104
            return self._session._client_capabilities
105
        elif whose in ('agent', 'server'):
106
            return self._session._server_capabilities
107

  
108 136
    @property
109
    def capabilities(self):
137
    def client_capabilities(self):
110 138
        return self._session._client_capabilities
111 139

  
112 140
    @property
113 141
    def server_capabilities(self):
114 142
        return self._session._server_capabilities
143

  
144
    @property
145
    def session_id(self):
146
        return self._session.id
b/ncclient/operations/__init__.py
12 12
# See the License for the specific language governing permissions and
13 13
# limitations under the License.
14 14

  
15
'NETCONF protocol operations'
16

  
17 15
from errors import OperationError, MissingCapabilityError
18
from rpc import RPCError
19
from retrieve import Get, GetConfig
20
from edit import EditConfig, CopyConfig, DeleteConfig, Validate, Commit, DiscardChanges
16
from rpc import RPC, RPCReply, RPCError
17
from retrieve import Get, GetConfig, GetReply
18
from edit import EditConfig, CopyConfig, DeleteConfig, Validate, Commit, DiscardChanges, ConfirmedCommit
21 19
from session import CloseSession, KillSession
22 20
from lock import Lock, Unlock, LockContext
23
from subscribe import CreateSubscription
21
#from subscribe import CreateSubscription
24 22

  
25 23
OPERATIONS = {
26 24
    'get': Get,
......
38 36
}
39 37

  
40 38
__all__ = [
39
    'RPC',
40
    'RPCReply',
41 41
    'RPCError',
42 42
    'OPERATIONS',
43 43
    'Get',
44 44
    'GetConfig',
45
    'GetReply',
45 46
    'EditConfig',
46 47
    'CopyConfig',
47 48
    'Validate',
48 49
    'Commit',
50
    'ConfirmedCommit'
49 51
    'DiscardChanges',
50 52
    'DeleteConfig',
51 53
    'Lock',
52 54
    'Unlock',
53 55
    'LockContext',
54 56
    'CloseSession',
55
    'KillSession',
56
    'CreateSubscription',
57
    'KillSession'
57 58
]
b/ncclient/operations/edit.py
21 21
"Operations related to configuration editing"
22 22

  
23 23
class EditConfig(RPC):
24
    
25
    # tested: no
26
    # combed: yes
27
    
24

  
28 25
    SPEC = {'tag': 'edit-config', 'subtree': []}
29
    
26

  
30 27
    def request(self, target=None, config=None, default_operation=None,
31 28
                test_option=None, error_option=None):
32 29
        util.one_of(target, config)
......
52 49
                'tag': 'error-option',
53 50
                'text': error_option
54 51
                })
52
        return self._request(spec)
55 53

  
56 54

  
57 55
class DeleteConfig(RPC):
58
    
59
    # tested: no
60
    # combed: yes
61
    
56

  
62 57
    SPEC = {'tag': 'delete-config', 'subtree': []}
63
    
58

  
64 59
    def request(self, target):
65 60
        spec = DeleteConfig.SPEC.copy()
66
        spec['subtree'].append(util.store_or_url('source', source, self._assert))
61
        spec['subtree'].append(util.store_or_url('target', target, self._assert))
67 62
        return self._request(spec)
68 63

  
69 64

  
70 65
class CopyConfig(RPC):
71
    
72
    # tested: no
73
    # combed: yes
74
    
66

  
75 67
    SPEC = {'tag': 'copy-config', 'subtree': []}
76
    
68

  
77 69
    def request(self, source, target):
78 70
        spec = CopyConfig.SPEC.copy()
79 71
        spec['subtree'].append(util.store_or_url('source', source, self._assert))
80
        spec['subtree'].append(util.store_or_url('target', source, self._assert))
72
        spec['subtree'].append(util.store_or_url('target', target, self._assert))
81 73
        return self._request(spec)
82 74

  
83 75

  
84 76
class Validate(RPC):
85
    
86
    # tested: no
87
    # combed: yes
88
    
89
    'config attr shd not include <config> root'
90
    
77

  
91 78
    DEPENDS = [':validate']
92
    
79

  
93 80
    SPEC = {'tag': 'validate', 'subtree': []}
94
    
95
    def request(self, source=None, config=None):
96
        util.one_of(source, config)
81

  
82
    def request(self, source):
83
        # determine if source is a <config> element
97 84
        spec = Validate.SPEC.copy()
98
        if config is None:
85
        try:
86
            spec['subtree'].append({
87
                'tag': 'source',
88
                'subtree': content.validated_root(config, ('config', content.qualify('config')))
89
                })
90
        except ContentError:
99 91
            spec['subtree'].append(util.store_or_url('source', source, self._assert))
100
        else:
101
            spec['subtree'].append(content.validated_root(config, 'config'))
102 92
        return self._request(spec)
103 93

  
104 94

  
105 95
class Commit(RPC):
106
    
107
    # tested: no
108
    # combed: yes
109
    
96

  
110 97
    DEPENDS = [':candidate']
111
    
98

  
112 99
    SPEC = {'tag': 'commit', 'subtree': []}
113
    
100

  
114 101
    def _parse_hook(self):
115 102
        pass
116
    
117
    def request(self, confirmed=False, timeout=None):
103

  
104
    def request(self, confirmed=False):
118 105
        spec = SPEC.copy()
119 106
        if confirmed:
120 107
            self._assert(':confirmed-commit')
......
128 115

  
129 116

  
130 117
class DiscardChanges(RPC):
131
    
132
    # tested: no
133
    # combed: yes
134
    
118

  
135 119
    DEPENDS = [':candidate']
136
    
120

  
137 121
    SPEC = {'tag': 'discard-changes'}
138 122

  
139 123

  
140 124
class ConfirmedCommit(Commit):
141 125
    "psuedo-op"
142
    
143
    # tested: no
144
    # combed: yes
145
    
126

  
146 127
    DEPENDS = [':candidate', ':confirmed-commit']
147
    
148
    def request(self, timeout=None):
149
        "Commit changes; requireing that a confirming commit follow"
150
        return Commit.request(self, confirmed=True, timeout=timeout)
151
    
128

  
129
    def request(self):
130
        "Commit changes requiring that a confirm/discard follow"
131
        return Commit.request(self, confirmed=True)
132

  
152 133
    def confirm(self):
153
        "Make the confirming commit"
134
        "Confirm changes"
154 135
        return Commit.request(self, confirmed=True)
155
    
136

  
156 137
    def discard(self):
138
        "Discard changes"
157 139
        return DiscardChanges(self.session, self.async, self.timeout).request()
b/ncclient/operations/errors.py
19 19

  
20 20
class MissingCapabilityError(NCClientError):
21 21
    pass
22

  
b/ncclient/operations/lock.py
17 17
from rpc import RPC
18 18

  
19 19
class Lock(RPC):
20
    
21
    # tested: no
22
    # combed: yes
23
    
20

  
24 21
    SPEC = {
25 22
        'tag': 'lock',
26 23
        'subtree': {
......
28 25
            'subtree': {'tag': None }
29 26
        }
30 27
    }
31
    
32
    def request(self, target):
28

  
29
    def request(self, target, *args, **kwds):
33 30
        spec = Lock.SPEC.copy()
34 31
        spec['subtree']['subtree']['tag'] = target
35
        return self._request(spec)
32
        return self._request(spec, *args, **kwds)
36 33

  
37 34

  
38 35
class Unlock(RPC):
39
    
40
    # tested: no
41
    # combed: yes
42
    
36

  
43 37
    SPEC = {
44 38
        'tag': 'unlock',
45 39
        'subtree': {
......
47 41
            'subtree': {'tag': None }
48 42
        }
49 43
    }
50
    
51
    def request(self, target):
44

  
45
    def request(self, target, *args, **kwds):
52 46
        spec = Unlock.SPEC.copy()
53 47
        spec['subtree']['subtree']['tag'] = target
54
        return self._request(spec)
48
        return self._request(spec, *args, **kwds)
55 49

  
56 50

  
57 51
class LockContext:
58
    
59
    # tested: no
60
    # combed: yes
61
    
52

  
62 53
    def __init__(self, session, target):
63 54
        self.session = session
64 55
        self.target = target
65
    
56

  
66 57
    def __enter__(self):
67 58
        reply = Lock(self.session).request(self.target)
68 59
        if not reply.ok:
69 60
            raise reply.error
70 61
        else:
71 62
            return self
72
    
63

  
73 64
    def __exit__(self, *args):
74 65
        reply = Unlock(session).request(self.target)
75 66
        if not reply.ok:
b/ncclient/operations/retrieve.py
19 19
import util
20 20

  
21 21
class GetReply(RPCReply):
22
    
23
    'Adds data attribute'
24
    
25
    # tested: no
26
    # combed: yes
27
    
22

  
23
    """Adds attributes for the *<data>* element to :class:`RPCReply`, pertinent
24
    to the *<get>* or *<get-config>* operations."""
25

  
28 26
    def _parsing_hook(self, root):
29 27
        self._data = None
30 28
        if not self._errors:
31
            self._data = content.find(root, 'data', nslist=[content.BASE_NS, content.CISCO_BS])
32
    
29
            self._data = content.find(root, 'data',
30
                                      nslist=[content.BASE_NS,
31
                                              content.CISCO_BS])
32

  
33 33
    @property
34
    def data(self):
34
    def data_ele(self):
35
        "As an :class:`~xml.etree.ElementTree.Element`"
35 36
        if not self._parsed:
36 37
            self.parse()
37 38
        return self._data
38 39

  
40
    @property
41
    def data_xml(self):
42
        "As an XML string"
43
        if not self._parsed:
44
            self.parse()
45
        return content.ele2xml(self._data)
46

  
47
    data = data_ele
48

  
49

  
39 50
class Get(RPC):
40
    
41
    # tested: no
42
    # combed: yes
43
    
51

  
52
    "*<get>* RPC"
53

  
44 54
    SPEC = {
45 55
        'tag': 'get',
46 56
        'subtree': []
47 57
    }
48
    
58

  
49 59
    REPLY_CLS = GetReply
50
    
60

  
51 61
    def request(self, filter=None):
52 62
        spec = Get.SPEC.copy()
53 63
        if filter is not None:
......
57 67

  
58 68
class GetConfig(RPC):
59 69

  
60
    # tested: no
61
    # combed: yes
62
    
70
    "*<get-config>* RPC"
71

  
63 72
    SPEC = {
64 73
        'tag': 'get-config',
65 74
        'subtree': []
66 75
    }
67
    
76

  
68 77
    REPLY_CLS = GetReply
69
    
78

  
70 79
    def request(self, source, filter=None):
71
        """
72
        `filter` has to be a tuple of (type, criteria)
73
        The type may be one of 'xpath' or 'subtree'
74
        The criteria may be an ElementTree.Element, an XML fragment, or tree specification
75
        """
76 80
        spec = GetConfig.SPEC.copy()
77 81
        spec['subtree'].append(util.store_or_url('source', source, self._assert))
78 82
        if filter is not None:
b/ncclient/operations/rpc.py
17 17
from weakref import WeakValueDictionary
18 18

  
19 19
from ncclient import content
20
from ncclient.capabilities import check
20 21
from ncclient.transport import SessionListener
21 22

  
22 23
from errors import OperationError
......
27 28

  
28 29
class RPCReply:
29 30

  
31
    """Represents an *<rpc-reply>*. Only concerns itself with whether the
32
    operation was successful. Note that if the reply has not yet been parsed
33
    there is a one-time parsing overhead to accessing the :attr:`ok` and
34
    :attr:`error`/:attr:`errors` attributes."""
35

  
30 36
    def __init__(self, raw):
31 37
        self._raw = raw
32 38
        self._parsed = False
......
36 42
    def __repr__(self):
37 43
        return self._raw
38 44

  
39
    def _parsing_hook(self, root): pass
45
    def _parsing_hook(self, root):
46
        """Subclass can implement.
47

  
48
        :type root: :class:`~xml.etree.ElementTree.Element`
49
        """
50
        pass
40 51

  
41 52
    def parse(self):
53
        """Parse the *<rpc-reply>*"""
42 54
        if self._parsed:
43 55
            return
44 56
        root = self._root = content.xml2ele(self._raw) # <rpc-reply> element
......
65 77

  
66 78
    @property
67 79
    def xml(self):
68
        '<rpc-reply> as returned'
80
        "*<rpc-reply>* as returned"
69 81
        return self._raw
70 82

  
71 83
    @property
72 84
    def ok(self):
85
        "Boolean value indicating if there were no errors."
73 86
        if not self._parsed:
74 87
            self.parse()
75 88
        return not self._errors # empty list => false
76 89

  
77 90
    @property
78 91
    def error(self):
92
        "Short for :attr:`errors`[0], returning :const:`None` if there were no errors."
79 93
        if not self._parsed:
80 94
            self.parse()
81 95
        if self._errors:
......
85 99

  
86 100
    @property
87 101
    def errors(self):
88
        'List of RPCError objects. Will be empty if no <rpc-error> elements in reply.'
102
        "List of :class:`RPCError` objects. Will be empty if there were no :class:`<rpc-error>` elements in reply."
89 103
        if not self._parsed:
90 104
            self.parse()
91 105
        return self._errors
......
93 107

  
94 108
class RPCError(OperationError): # raise it if you like
95 109

  
110
    """Represents an *<rpc-error>*. It is an instance of :exc:`OperationError`
111
    so it can be raised like any other exception."""
112

  
96 113
    def __init__(self, err_dict):
97 114
        self._dict = err_dict
98 115
        if self.message is not None:
......
102 119

  
103 120
    @property
104 121
    def type(self):
122
        "`string` represeting *error-type* element"
105 123
        return self.get('error-type', None)
106 124

  
107 125
    @property
108 126
    def severity(self):
127
        "`string` represeting *error-severity* element"
109 128
        return self.get('error-severity', None)
110 129

  
111 130
    @property
112 131
    def tag(self):
132
        "`string` represeting *error-tag* element"
113 133
        return self.get('error-tag', None)
114 134

  
115 135
    @property
116 136
    def path(self):
137
        "`string` or :const:`None`; represeting *error-path* element"
117 138
        return self.get('error-path', None)
118 139

  
119 140
    @property
120 141
    def message(self):
142
        "`string` or :const:`None`; represeting *error-message* element"
121 143
        return self.get('error-message', None)
122 144

  
123 145
    @property
124 146
    def info(self):
147
        "`string` or :const:`None`, represeting *error-info* element"
125 148
        return self.get('error-info', None)
126 149

  
127 150
    ## dictionary interface
......
151 174

  
152 175
class RPCReplyListener(SessionListener):
153 176

  
177
    # internal use
178

  
154 179
    # one instance per session
155 180
    def __new__(cls, session):
156 181
        instance = session.get_listener_instance(cls)
......
191 216
            else:
192 217
                logger.warning('<rpc-reply> without message-id received: %s' % raw)
193 218
        logger.debug('delivering to %r' % rpc)
194
        rpc.deliver(raw)
219
        rpc.deliver_reply(raw)
195 220

  
196 221
    def errback(self, err):
197 222
        for rpc in self._id2rpc.values():
198
            rpc.error(err)
223
            rpc.deliver_error(err)
199 224

  
200 225

  
201 226
class RPC(object):
202 227

  
228
    "Directly corresponds to *<rpc>* requests. Handles making the request, and taking delivery of the reply."
229

  
230
    # : Subclasses can specify their dependencies on capabilities. List of URI's
231
    # or abbreviated names, e.g. ':writable-running'. These are verified at the
232
    # time of object creation. If the capability is not available, a
233
    # :exc:`MissingCapabilityError` is raised.
203 234
    DEPENDS = []
235

  
236
    # : Subclasses can specify a different reply class, but it must be a
237
    # subclass of :class:`RPCReply`.
204 238
    REPLY_CLS = RPCReply
205 239

  
206 240
    def __init__(self, session, async=False, timeout=None):
207
        if not session.can_pipeline:
208
            raise UserWarning('Asynchronous mode not supported for this device/session')
209 241
        self._session = session
210 242
        try:
211 243
            for cap in self.DEPENDS:
......
221 253
        self._listener.register(self._id, self)
222 254
        self._reply = None
223 255
        self._error = None
224
        self._reply_event = Event()
256
        self._event = Event()
225 257

  
226 258
    def _build(self, opspec):
227
        "TODO: docstring"
259
        # internal
228 260
        spec = {
229 261
            'tag': content.qualify('rpc'),
230 262
            'attrib': {'message-id': self._id},
231
            'subtree': opspec
263
            'subtree': [ opspec ]
232 264
            }
233 265
        return content.dtree2xml(spec)
234 266

  
235 267
    def _request(self, op):
268
        """Subclasses call this method to make the RPC request.
269

  
270
        In asynchronous mode, returns an :class:`~threading.Event` which is set
271
        when the reply has been received or an error occured. It is prudent,
272
        therefore, to check the :attr:`error` attribute before accesing
273
        :attr:`reply`.
274

  
275
        Otherwise, waits until the reply is received and returns
276
        :class:`RPCReply`.
277

  
278
        :arg opspec: :ref:`dtree` for the operation
279
        :type opspec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
280
        :rtype: :class:`~threading.Event` or :class:`RPCReply`
281
        """
236 282
        req = self._build(op)
237 283
        self._session.send(req)
238 284
        if self._async:
239
            return self._reply_event
285
            return self._event
240 286
        else:
241
            self._reply_event.wait(self._timeout)
242
            if self._reply_event.isSet():
287
            self._event.wait(self._timeout)
288
            if self._event.isSet():
243 289
                if self._error:
244 290
                    raise self._error
245 291
                self._reply.parse()
......
247 293
            else:
248 294
                raise ReplyTimeoutError
249 295

  
250
    def request(self):
251
        return self._request(self.SPEC)
296
    def request(self, *args, **kwds):
297
        "Subclasses implement this method. Here, the operation is to be constructed as a :ref:`dtree`, and the result of :meth:`_request` returned."
298
        return self._request(self.SPEC, *args, **kwds)
252 299

  
253 300
    def _delivery_hook(self):
254
        'For subclasses'
301
        """Subclasses can implement this method. Will be called after
302
        initialising the :attr:`reply` or :attr:`error` attribute and before
303
        setting the :attr:`event`"""
255 304
        pass
256 305

  
257 306
    def _assert(self, capability):
307
        """Subclasses can use this method to verify that a capability is available
308
        with the NETCONF server, before making a request that requires it. A
309
        :class:`MissingCapabilityError` will be raised if the capability is not
310
        available."""
258 311
        if capability not in self._session.server_capabilities:
259 312
            raise MissingCapabilityError('Server does not support [%s]' % cap)
260 313

  
261
    def deliver(self, raw):
314
    def deliver_reply(self, raw):
315
        # internal use
262 316
        self._reply = self.REPLY_CLS(raw)
263 317
        self._delivery_hook()
264
        self._reply_event.set()
318
        self._event.set()
265 319

  
266
    def error(self, err):
320
    def deliver_error(self, err):
321
        # internal use
267 322
        self._error = err
268
        self._reply_event.set()
269

  
270
    @property
271
    def has_reply(self):
272
        return self._reply_event.is_set()
323
        self._delivery_hook()
324
        self._event.set()
273 325

  
274 326
    @property
275 327
    def reply(self):
276
        if self.error:
277
            raise self._error
328
        ":class:`RPCReply` element if reply has been received or :const:`None`"
278 329
        return self._reply
279 330

  
280 331
    @property
332
    def error(self):
333
        """:exc:`Exception` type if an error occured or :const:`None`.
334

  
335
        This attribute should be checked if the request was made asynchronously,
336
        so that it can be determined if :attr:`event` being set is because of a
337
        reply or error.
338

  
339
        .. note::
340
            This represents an error which prevented a reply from being
341
            received. An *<rpc-error>* does not fall in that category -- see
342
            :class:`RPCReply` for that.
343
        """
344
        return self._error
345

  
346
    @property
281 347
    def id(self):
348
        "The *message-id* for this RPC"
282 349
        return self._id
283 350

  
284 351
    @property
285 352
    def session(self):
353
        """The :class:`~ncclient.transport.Session` object associated with this
354
        RPC"""
286 355
        return self._session
287 356

  
288 357
    @property
289
    def reply_event(self):
290
        return self._reply_event
358
    def event(self):
359
        """:class:`~threading.Event` that is set when reply has been received or
360
        error occured."""
361
        return self._event
362

  
363
    def set_async(self, async=True):
364
        """Set asynchronous mode for this RPC."""
365
        self._async = async
366
        if async and not session.can_pipeline:
367
            raise UserWarning('Asynchronous mode not supported for this device/session')
368

  
369
    def set_timeout(self, timeout):
370
        """Set the timeout for synchronous waiting defining how long the RPC
371
        request will block on a reply before raising an error."""
372
        self._timeout = timeout
291 373

  
292
    def set_async(self, bool): self._async = bool
374
    #: Whether this RPC is asynchronous
293 375
    async = property(fget=lambda self: self._async, fset=set_async)
294 376

  
295
    def set_timeout(self, timeout): self._timeout = timeout
377
    #: Timeout for synchronous waiting
296 378
    timeout = property(fget=lambda self: self._timeout, fset=set_timeout)
b/ncclient/operations/session.py
17 17
from rpc import RPC
18 18

  
19 19
class CloseSession(RPC):
20
    # tested: no
21
    # combed: yes
22
    
20

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

  
23 23
    SPEC = { 'tag': 'close-session' }
24
    
25
    def _delivery_hook(self):
24

  
25
    def _delivsery_hook(self):
26 26
        self.session.close()
27 27

  
28 28

  
29 29
class KillSession(RPC):
30
    # tested: no
31
    # combed: yes
32
    
30

  
31
    "*<kill-session>* RPC."
32

  
33 33
    SPEC = {
34 34
        'tag': 'kill-session',
35 35
        'subtree': []
36 36
    }
37
    
38
    def request(self, session_id):
37

  
38
    def request(self, session_id, *args, **kwds):
39 39
        spec = KillSession.SPEC.copy()
40 40
        if not isinstance(session_id, basestring): # just making sure...
41 41
            session_id = str(session_id)
......
43 43
            'tag': 'session-id',
44 44
            'text': session_id
45 45
        })
46
        return self._request(spec)
46
        return self._request(spec, *args, **kwds)
b/ncclient/operations/util.py
50 50
            'subtree': criteria
51 51
        }
52 52
    else:
53
        rep = content.validated_element(spec, 'filter', 'type')
53
        rep = content.validated_element(spec, ['filter', content.qualify('filter')],
54
                                        attrs=[('type', content.qualify('type'))])
54 55
        try:
55 56
            type = rep['type']
56 57
        except KeyError:
b/ncclient/transport/ssh.py
278 278

  
279 279
        if saved_exception is not None:
280 280
            # need pep-3134 to do this right
281
            raise SSHAuthenticationError(repr(saved_exception))
281
            raise AuthenticationError(repr(saved_exception))
282 282

  
283
        raise SSHAuthenticationError('No authentication methods available')
283
        raise AuthenticationError('No authentication methods available')
284 284

  
285 285
    def run(self):
286 286
        chan = self._channel

Also available in: Unified diff