From a7cb58ce92ff09d3e758c218b2bb58cc16079d99 Mon Sep 17 00:00:00 2001 From: Shikhar Bhushan Date: Sat, 16 May 2009 18:46:19 +0000 Subject: [PATCH] git-svn-id: http://ncclient.googlecode.com/svn/trunk@122 6dbcf712-26ac-11de-a2f3-1373824ab735 --- ncclient/capabilities.py | 75 ++++++++++++++------- ncclient/content.py | 8 ++- ncclient/manager.py | 78 ++++++++++++++------- ncclient/operations/__init__.py | 17 ++--- ncclient/operations/edit.py | 90 ++++++++++--------------- ncclient/operations/errors.py | 1 - ncclient/operations/lock.py | 31 +++------ ncclient/operations/retrieve.py | 54 ++++++++------- ncclient/operations/rpc.py | 142 ++++++++++++++++++++++++++++++--------- ncclient/operations/session.py | 22 +++--- ncclient/operations/util.py | 3 +- ncclient/transport/ssh.py | 4 +- 12 files changed, 324 insertions(+), 201 deletions(-) diff --git a/ncclient/capabilities.py b/ncclient/capabilities.py index 99153c8..6e1f78d 100644 --- a/ncclient/capabilities.py +++ b/ncclient/capabilities.py @@ -14,54 +14,81 @@ def abbreviate(uri): if uri.startswith('urn:ietf:params:netconf:capability:'): - return (':' + uri.split(':')[5]) + return ':' + uri.split(':')[5] + elif uri.startswith('urn:ietf:params:netconf:base:'): + return ':base' -def schemes(uri): - return uri.partition("?scheme=")[2].split(',') +def version(uri): + if uri.startswith('urn:ietf:params:netconf:capability:'): + return uri.split(':')[6] + elif uri.startswith('urn:ietf:params:netconf:base:'): + return uri.split(':')[5] class Capabilities: - - """Represent the capabilities of client or server. Also facilitates using - abbreviated capability names in addition to complete URI. - """ - + def __init__(self, capabilities=None): self._dict = {} if isinstance(capabilities, dict): self._dict = capabilities elif isinstance(capabilities, list): for uri in capabilities: - self._dict[uri] = abbreviate(uri) - + self._dict[uri] = (abbreviate(uri), version(uri)) + def __contains__(self, key): - return ( key in self._dict ) or ( key in self._dict.values() ) - + if key in self._dict: + return True + for info in self._dict.values(): + if key == info[0]: + return True + return False + def __iter__(self): return self._dict.keys().__iter__() - + def __repr__(self): return repr(self._dict.keys()) - + def __list__(self): return self._dict.keys() - - def add(self, uri, shorthand=None): - if shorthand is None: - shorthand = abbreviate(uri) - self._dict[uri] = shorthand - + + def add(self, uri, info=None): + if info is None: + info = (abbreviate(uri), version(uri)) + self._dict[uri] = info + set = add - + def remove(self, key): if key in self._dict: del self._dict[key] else: for uri in self._dict: - if self._dict[uri] == key: + if key in self._dict[uri]: del self._dict[uri] break -# : the capabilities currently supported by ncclient + def get_uri(self, shortname): + for uri, info in self._dict.items(): + if info[0] == shortname: + return uri + + def url_schemes(self): + url_uri = get_uri(':url') + if url_uri is None: + return [] + else: + return url_uri.partition("?scheme=")[2].split(',') + + def version(self, key): + try: + return self._dict[key][1] + except KeyError: + for uri, info in self._dict.items(): + if info[0] == key: + return info[1] + + +#: the capabilities supported by NCClient CAPABILITIES = Capabilities([ 'urn:ietf:params:netconf:base:1.0', 'urn:ietf:params:netconf:capability:writable-running:1.0', @@ -74,4 +101,4 @@ CAPABILITIES = Capabilities([ 'urn:ietf:params:netconf:capability:xpath:1.0', #'urn:ietf:params:netconf:capability:notification:1.0', # TODO #'urn:ietf:params:netconf:capability:interleave:1.0' # theoretically already supported -]) \ No newline at end of file +]) diff --git a/ncclient/content.py b/ncclient/content.py index 434231d..9685514 100644 --- a/ncclient/content.py +++ b/ncclient/content.py @@ -12,6 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. + +"""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. +""" + from cStringIO import StringIO from xml.etree import cElementTree as ET @@ -23,9 +27,9 @@ class ContentError(NCClientError): ### Namespace-related -# : Base NETCONf namespace +#: Base NETCONf namespace BASE_NS = 'urn:ietf:params:xml:ns:netconf:base:1.0' -# : ... and this is BASE_NS according to Cisco devices tested +#: ... and this is BASE_NS according to Cisco devices tested CISCO_BS = 'urn:ietf:params:netconf:base:1.0' try: diff --git a/ncclient/manager.py b/ncclient/manager.py index 14d6443..cd8ad03 100644 --- a/ncclient/manager.py +++ b/ncclient/manager.py @@ -13,7 +13,7 @@ # limitations under the License. import capabilities -import operations +from operations import OPERATIONS import transport @@ -25,7 +25,12 @@ def ssh_connect(*args, **kwds): connect = ssh_connect # default session type -RAISE_ALL, RAISE_ERROR, RAISE_NONE = range(3) +#: Raise all errors +RAISE_ALL = 0 +#: +RAISE_ERR = 1 +#: +RAISE_NONE = 2 class Manager: @@ -33,13 +38,13 @@ class Manager: def __init__(self, session): self._session = session - self._rpc_error_handling = RAISE_ALL + self._rpc_error_action = RAISE_ALL - def set_rpc_error_option(self, option): + def set_rpc_error_action(self, action): self._rpc_error_handling = option def do(self, op, *args, **kwds): - op = operations.OPERATIONS[op](self._session) + op = OPERATIONS[op](self._session) reply = op.request(*args, **kwds) if not reply.ok: if self._raise == RAISE_ALL: @@ -59,25 +64,54 @@ class Manager: def locked(self, target): """Returns a context manager for use with the 'with' statement. - `target` is the datastore to lock, e.g. 'candidate + + :arg target: name of the datastore to lock + :type target: `string` """ return operations.LockContext(self._session, target) - get = lambda self, *args, **kwds: self.do('get', *args, **kwds).data + def get(self, filter=None): + pass + + def get_config(self, source, filter=None): + pass + + def copy_config(self, source, target): + pass + + def validate(self, source): + pass + + def commit(self, target): + pass + + def discard_changes(self): + pass + + def delete_config(self, target): + pass - get_config = lambda self, *args, **kwds: self.do('get-config', *args, **kwds).data + def lock(self, target): + pass - edit_config = lambda self, *args, **kwds: self.do('edit-config', *args, **kwds) + def unlock(self, target): + pass - copy_config = lambda self, *args, **kwds: self.do('copy-config', *args, **kwds) + def close_session(self): + pass - validate = lambda self, *args, **kwds: self.do('validate', *args, **kwds) + def kill_session(self, session_id): + pass - commit = lambda self, *args, **kwds: self.do('commit', *args, **kwds) + def confirmed_commit(self, timeout=None): + pass - discard_changes = lambda self, *args, **kwds: self.do('discard-changes', *args, **kwds) + def confirm(self): + # give confirmation + pass - delete_config = lambda self, *args, **kwds: self.do('delete-config', *args, **kwds) + def discard_changes(self): + pass lock = lambda self, *args, **kwds: self.do('lock', *args, **kwds) @@ -90,8 +124,8 @@ class Manager: def close(self): try: # try doing it clean self.close_session() - except: - pass + except Exception as e: + logger.debug('error doing -- %r' % e) if self._session.connected: # if that didn't work... self._session.close() @@ -99,16 +133,14 @@ class Manager: def session(self, session): return self._session - def get_capabilities(self, whose): - if whose in ('manager', 'client'): - return self._session._client_capabilities - elif whose in ('agent', 'server'): - return self._session._server_capabilities - @property - def capabilities(self): + def client_capabilities(self): return self._session._client_capabilities @property def server_capabilities(self): return self._session._server_capabilities + + @property + def session_id(self): + return self._session.id diff --git a/ncclient/operations/__init__.py b/ncclient/operations/__init__.py index f8cedd6..b9ceafb 100644 --- a/ncclient/operations/__init__.py +++ b/ncclient/operations/__init__.py @@ -12,15 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -'NETCONF protocol operations' - from errors import OperationError, MissingCapabilityError -from rpc import RPCError -from retrieve import Get, GetConfig -from edit import EditConfig, CopyConfig, DeleteConfig, Validate, Commit, DiscardChanges +from rpc import RPC, RPCReply, RPCError +from retrieve import Get, GetConfig, GetReply +from edit import EditConfig, CopyConfig, DeleteConfig, Validate, Commit, DiscardChanges, ConfirmedCommit from session import CloseSession, KillSession from lock import Lock, Unlock, LockContext -from subscribe import CreateSubscription +#from subscribe import CreateSubscription OPERATIONS = { 'get': Get, @@ -38,20 +36,23 @@ OPERATIONS = { } __all__ = [ + 'RPC', + 'RPCReply', 'RPCError', 'OPERATIONS', 'Get', 'GetConfig', + 'GetReply', 'EditConfig', 'CopyConfig', 'Validate', 'Commit', + 'ConfirmedCommit' 'DiscardChanges', 'DeleteConfig', 'Lock', 'Unlock', 'LockContext', 'CloseSession', - 'KillSession', - 'CreateSubscription', + 'KillSession' ] diff --git a/ncclient/operations/edit.py b/ncclient/operations/edit.py index 4651c6e..806f9e9 100644 --- a/ncclient/operations/edit.py +++ b/ncclient/operations/edit.py @@ -21,12 +21,9 @@ import util "Operations related to configuration editing" class EditConfig(RPC): - - # tested: no - # combed: yes - + SPEC = {'tag': 'edit-config', 'subtree': []} - + def request(self, target=None, config=None, default_operation=None, test_option=None, error_option=None): util.one_of(target, config) @@ -52,69 +49,59 @@ class EditConfig(RPC): 'tag': 'error-option', 'text': error_option }) + return self._request(spec) class DeleteConfig(RPC): - - # tested: no - # combed: yes - + SPEC = {'tag': 'delete-config', 'subtree': []} - + def request(self, target): spec = DeleteConfig.SPEC.copy() - spec['subtree'].append(util.store_or_url('source', source, self._assert)) + spec['subtree'].append(util.store_or_url('target', target, self._assert)) return self._request(spec) class CopyConfig(RPC): - - # tested: no - # combed: yes - + SPEC = {'tag': 'copy-config', 'subtree': []} - + def request(self, source, target): spec = CopyConfig.SPEC.copy() spec['subtree'].append(util.store_or_url('source', source, self._assert)) - spec['subtree'].append(util.store_or_url('target', source, self._assert)) + spec['subtree'].append(util.store_or_url('target', target, self._assert)) return self._request(spec) class Validate(RPC): - - # tested: no - # combed: yes - - 'config attr shd not include root' - + DEPENDS = [':validate'] - + SPEC = {'tag': 'validate', 'subtree': []} - - def request(self, source=None, config=None): - util.one_of(source, config) + + def request(self, source): + # determine if source is a element spec = Validate.SPEC.copy() - if config is None: + try: + spec['subtree'].append({ + 'tag': 'source', + 'subtree': content.validated_root(config, ('config', content.qualify('config'))) + }) + except ContentError: spec['subtree'].append(util.store_or_url('source', source, self._assert)) - else: - spec['subtree'].append(content.validated_root(config, 'config')) return self._request(spec) class Commit(RPC): - - # tested: no - # combed: yes - + DEPENDS = [':candidate'] - + SPEC = {'tag': 'commit', 'subtree': []} - + def _parse_hook(self): pass - - def request(self, confirmed=False, timeout=None): + + def request(self, confirmed=False): spec = SPEC.copy() if confirmed: self._assert(':confirmed-commit') @@ -128,30 +115,25 @@ class Commit(RPC): class DiscardChanges(RPC): - - # tested: no - # combed: yes - + DEPENDS = [':candidate'] - + SPEC = {'tag': 'discard-changes'} class ConfirmedCommit(Commit): "psuedo-op" - - # tested: no - # combed: yes - + DEPENDS = [':candidate', ':confirmed-commit'] - - def request(self, timeout=None): - "Commit changes; requireing that a confirming commit follow" - return Commit.request(self, confirmed=True, timeout=timeout) - + + def request(self): + "Commit changes requiring that a confirm/discard follow" + return Commit.request(self, confirmed=True) + def confirm(self): - "Make the confirming commit" + "Confirm changes" return Commit.request(self, confirmed=True) - + def discard(self): + "Discard changes" return DiscardChanges(self.session, self.async, self.timeout).request() diff --git a/ncclient/operations/errors.py b/ncclient/operations/errors.py index 836fcd0..bf60cd8 100644 --- a/ncclient/operations/errors.py +++ b/ncclient/operations/errors.py @@ -19,4 +19,3 @@ class OperationError(NCClientError): class MissingCapabilityError(NCClientError): pass - diff --git a/ncclient/operations/lock.py b/ncclient/operations/lock.py index 2227db9..94528ab 100644 --- a/ncclient/operations/lock.py +++ b/ncclient/operations/lock.py @@ -17,10 +17,7 @@ from rpc import RPC class Lock(RPC): - - # tested: no - # combed: yes - + SPEC = { 'tag': 'lock', 'subtree': { @@ -28,18 +25,15 @@ class Lock(RPC): 'subtree': {'tag': None } } } - - def request(self, target): + + def request(self, target, *args, **kwds): spec = Lock.SPEC.copy() spec['subtree']['subtree']['tag'] = target - return self._request(spec) + return self._request(spec, *args, **kwds) class Unlock(RPC): - - # tested: no - # combed: yes - + SPEC = { 'tag': 'unlock', 'subtree': { @@ -47,29 +41,26 @@ class Unlock(RPC): 'subtree': {'tag': None } } } - - def request(self, target): + + def request(self, target, *args, **kwds): spec = Unlock.SPEC.copy() spec['subtree']['subtree']['tag'] = target - return self._request(spec) + return self._request(spec, *args, **kwds) class LockContext: - - # tested: no - # combed: yes - + def __init__(self, session, target): self.session = session self.target = target - + def __enter__(self): reply = Lock(self.session).request(self.target) if not reply.ok: raise reply.error else: return self - + def __exit__(self, *args): reply = Unlock(session).request(self.target) if not reply.ok: diff --git a/ncclient/operations/retrieve.py b/ncclient/operations/retrieve.py index 9ea4c95..30a69d1 100644 --- a/ncclient/operations/retrieve.py +++ b/ncclient/operations/retrieve.py @@ -19,35 +19,45 @@ from ncclient import content import util class GetReply(RPCReply): - - 'Adds data attribute' - - # tested: no - # combed: yes - + + """Adds attributes for the ** element to :class:`RPCReply`, pertinent + to the ** or ** operations.""" + def _parsing_hook(self, root): self._data = None if not self._errors: - self._data = content.find(root, 'data', nslist=[content.BASE_NS, content.CISCO_BS]) - + self._data = content.find(root, 'data', + nslist=[content.BASE_NS, + content.CISCO_BS]) + @property - def data(self): + def data_ele(self): + "As an :class:`~xml.etree.ElementTree.Element`" if not self._parsed: self.parse() return self._data + @property + def data_xml(self): + "As an XML string" + if not self._parsed: + self.parse() + return content.ele2xml(self._data) + + data = data_ele + + class Get(RPC): - - # tested: no - # combed: yes - + + "** RPC" + SPEC = { 'tag': 'get', 'subtree': [] } - + REPLY_CLS = GetReply - + def request(self, filter=None): spec = Get.SPEC.copy() if filter is not None: @@ -57,22 +67,16 @@ class Get(RPC): class GetConfig(RPC): - # tested: no - # combed: yes - + "** RPC" + SPEC = { 'tag': 'get-config', 'subtree': [] } - + REPLY_CLS = GetReply - + def request(self, source, filter=None): - """ - `filter` has to be a tuple of (type, criteria) - The type may be one of 'xpath' or 'subtree' - The criteria may be an ElementTree.Element, an XML fragment, or tree specification - """ spec = GetConfig.SPEC.copy() spec['subtree'].append(util.store_or_url('source', source, self._assert)) if filter is not None: diff --git a/ncclient/operations/rpc.py b/ncclient/operations/rpc.py index c82d872..4c20458 100644 --- a/ncclient/operations/rpc.py +++ b/ncclient/operations/rpc.py @@ -17,6 +17,7 @@ from uuid import uuid1 from weakref import WeakValueDictionary from ncclient import content +from ncclient.capabilities import check from ncclient.transport import SessionListener from errors import OperationError @@ -27,6 +28,11 @@ logger = logging.getLogger('ncclient.operations.rpc') class RPCReply: + """Represents an **. Only concerns itself with whether the + operation was successful. Note that if the reply has not yet been parsed + there is a one-time parsing overhead to accessing the :attr:`ok` and + :attr:`error`/:attr:`errors` attributes.""" + def __init__(self, raw): self._raw = raw self._parsed = False @@ -36,9 +42,15 @@ class RPCReply: def __repr__(self): return self._raw - def _parsing_hook(self, root): pass + def _parsing_hook(self, root): + """Subclass can implement. + + :type root: :class:`~xml.etree.ElementTree.Element` + """ + pass def parse(self): + """Parse the **""" if self._parsed: return root = self._root = content.xml2ele(self._raw) # element @@ -65,17 +77,19 @@ class RPCReply: @property def xml(self): - ' as returned' + "** as returned" return self._raw @property def ok(self): + "Boolean value indicating if there were no errors." if not self._parsed: self.parse() return not self._errors # empty list => false @property def error(self): + "Short for :attr:`errors`[0], returning :const:`None` if there were no errors." if not self._parsed: self.parse() if self._errors: @@ -85,7 +99,7 @@ class RPCReply: @property def errors(self): - 'List of RPCError objects. Will be empty if no elements in reply.' + "List of :class:`RPCError` objects. Will be empty if there were no :class:`` elements in reply." if not self._parsed: self.parse() return self._errors @@ -93,6 +107,9 @@ class RPCReply: class RPCError(OperationError): # raise it if you like + """Represents an **. It is an instance of :exc:`OperationError` + so it can be raised like any other exception.""" + def __init__(self, err_dict): self._dict = err_dict if self.message is not None: @@ -102,26 +119,32 @@ class RPCError(OperationError): # raise it if you like @property def type(self): + "`string` represeting *error-type* element" return self.get('error-type', None) @property def severity(self): + "`string` represeting *error-severity* element" return self.get('error-severity', None) @property def tag(self): + "`string` represeting *error-tag* element" return self.get('error-tag', None) @property def path(self): + "`string` or :const:`None`; represeting *error-path* element" return self.get('error-path', None) @property def message(self): + "`string` or :const:`None`; represeting *error-message* element" return self.get('error-message', None) @property def info(self): + "`string` or :const:`None`, represeting *error-info* element" return self.get('error-info', None) ## dictionary interface @@ -151,6 +174,8 @@ class RPCError(OperationError): # raise it if you like class RPCReplyListener(SessionListener): + # internal use + # one instance per session def __new__(cls, session): instance = session.get_listener_instance(cls) @@ -191,21 +216,28 @@ class RPCReplyListener(SessionListener): else: logger.warning(' without message-id received: %s' % raw) logger.debug('delivering to %r' % rpc) - rpc.deliver(raw) + rpc.deliver_reply(raw) def errback(self, err): for rpc in self._id2rpc.values(): - rpc.error(err) + rpc.deliver_error(err) class RPC(object): + "Directly corresponds to ** requests. Handles making the request, and taking delivery of the reply." + + # : Subclasses can specify their dependencies on capabilities. List of URI's + # or abbreviated names, e.g. ':writable-running'. These are verified at the + # time of object creation. If the capability is not available, a + # :exc:`MissingCapabilityError` is raised. DEPENDS = [] + + # : Subclasses can specify a different reply class, but it must be a + # subclass of :class:`RPCReply`. REPLY_CLS = RPCReply def __init__(self, session, async=False, timeout=None): - if not session.can_pipeline: - raise UserWarning('Asynchronous mode not supported for this device/session') self._session = session try: for cap in self.DEPENDS: @@ -221,25 +253,39 @@ class RPC(object): self._listener.register(self._id, self) self._reply = None self._error = None - self._reply_event = Event() + self._event = Event() def _build(self, opspec): - "TODO: docstring" + # internal spec = { 'tag': content.qualify('rpc'), 'attrib': {'message-id': self._id}, - 'subtree': opspec + 'subtree': [ opspec ] } return content.dtree2xml(spec) def _request(self, op): + """Subclasses call this method to make the RPC request. + + In asynchronous mode, returns an :class:`~threading.Event` which is set + when the reply has been received or an error occured. It is prudent, + therefore, to check the :attr:`error` attribute before accesing + :attr:`reply`. + + Otherwise, waits until the reply is received and returns + :class:`RPCReply`. + + :arg opspec: :ref:`dtree` for the operation + :type opspec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element` + :rtype: :class:`~threading.Event` or :class:`RPCReply` + """ req = self._build(op) self._session.send(req) if self._async: - return self._reply_event + return self._event else: - self._reply_event.wait(self._timeout) - if self._reply_event.isSet(): + self._event.wait(self._timeout) + if self._event.isSet(): if self._error: raise self._error self._reply.parse() @@ -247,50 +293,86 @@ class RPC(object): else: raise ReplyTimeoutError - def request(self): - return self._request(self.SPEC) + def request(self, *args, **kwds): + "Subclasses implement this method. Here, the operation is to be constructed as a :ref:`dtree`, and the result of :meth:`_request` returned." + return self._request(self.SPEC, *args, **kwds) def _delivery_hook(self): - 'For subclasses' + """Subclasses can implement this method. Will be called after + initialising the :attr:`reply` or :attr:`error` attribute and before + setting the :attr:`event`""" pass def _assert(self, capability): + """Subclasses can use this method to verify that a capability is available + with the NETCONF server, before making a request that requires it. A + :class:`MissingCapabilityError` will be raised if the capability is not + available.""" if capability not in self._session.server_capabilities: raise MissingCapabilityError('Server does not support [%s]' % cap) - def deliver(self, raw): + def deliver_reply(self, raw): + # internal use self._reply = self.REPLY_CLS(raw) self._delivery_hook() - self._reply_event.set() + self._event.set() - def error(self, err): + def deliver_error(self, err): + # internal use self._error = err - self._reply_event.set() - - @property - def has_reply(self): - return self._reply_event.is_set() + self._delivery_hook() + self._event.set() @property def reply(self): - if self.error: - raise self._error + ":class:`RPCReply` element if reply has been received or :const:`None`" return self._reply @property + def error(self): + """:exc:`Exception` type if an error occured or :const:`None`. + + This attribute should be checked if the request was made asynchronously, + so that it can be determined if :attr:`event` being set is because of a + reply or error. + + .. note:: + This represents an error which prevented a reply from being + received. An ** does not fall in that category -- see + :class:`RPCReply` for that. + """ + return self._error + + @property def id(self): + "The *message-id* for this RPC" return self._id @property def session(self): + """The :class:`~ncclient.transport.Session` object associated with this + RPC""" return self._session @property - def reply_event(self): - return self._reply_event + def event(self): + """:class:`~threading.Event` that is set when reply has been received or + error occured.""" + return self._event + + def set_async(self, async=True): + """Set asynchronous mode for this RPC.""" + self._async = async + if async and not session.can_pipeline: + raise UserWarning('Asynchronous mode not supported for this device/session') + + def set_timeout(self, timeout): + """Set the timeout for synchronous waiting defining how long the RPC + request will block on a reply before raising an error.""" + self._timeout = timeout - def set_async(self, bool): self._async = bool + #: Whether this RPC is asynchronous async = property(fget=lambda self: self._async, fset=set_async) - def set_timeout(self, timeout): self._timeout = timeout + #: Timeout for synchronous waiting timeout = property(fget=lambda self: self._timeout, fset=set_timeout) diff --git a/ncclient/operations/session.py b/ncclient/operations/session.py index e4116f9..94ed517 100644 --- a/ncclient/operations/session.py +++ b/ncclient/operations/session.py @@ -17,25 +17,25 @@ from rpc import RPC class CloseSession(RPC): - # tested: no - # combed: yes - + + "** RPC. The connection to NETCONF server is also closed." + SPEC = { 'tag': 'close-session' } - - def _delivery_hook(self): + + def _delivsery_hook(self): self.session.close() class KillSession(RPC): - # tested: no - # combed: yes - + + "** RPC." + SPEC = { 'tag': 'kill-session', 'subtree': [] } - - def request(self, session_id): + + def request(self, session_id, *args, **kwds): spec = KillSession.SPEC.copy() if not isinstance(session_id, basestring): # just making sure... session_id = str(session_id) @@ -43,4 +43,4 @@ class KillSession(RPC): 'tag': 'session-id', 'text': session_id }) - return self._request(spec) + return self._request(spec, *args, **kwds) diff --git a/ncclient/operations/util.py b/ncclient/operations/util.py index 20ba089..6cb6ccf 100644 --- a/ncclient/operations/util.py +++ b/ncclient/operations/util.py @@ -50,7 +50,8 @@ def build_filter(spec, capcheck=None): 'subtree': criteria } else: - rep = content.validated_element(spec, 'filter', 'type') + rep = content.validated_element(spec, ['filter', content.qualify('filter')], + attrs=[('type', content.qualify('type'))]) try: type = rep['type'] except KeyError: diff --git a/ncclient/transport/ssh.py b/ncclient/transport/ssh.py index 21a56ab..f84bb41 100644 --- a/ncclient/transport/ssh.py +++ b/ncclient/transport/ssh.py @@ -278,9 +278,9 @@ class SSHSession(Session): if saved_exception is not None: # need pep-3134 to do this right - raise SSHAuthenticationError(repr(saved_exception)) + raise AuthenticationError(repr(saved_exception)) - raise SSHAuthenticationError('No authentication methods available') + raise AuthenticationError('No authentication methods available') def run(self): chan = self._channel -- 1.7.10.4