From d771dffc8fc1160c4c7fe27636b6892667a471a6 Mon Sep 17 00:00:00 2001 From: Shikhar Bhushan Date: Tue, 12 May 2009 01:18:26 +0000 Subject: [PATCH] git-svn-id: http://ncclient.googlecode.com/svn/trunk@109 6dbcf712-26ac-11de-a2f3-1373824ab735 --- ncclient/capabilities.py | 9 +---- ncclient/content.py | 79 ++++++++++++++++++++------------------- ncclient/manager.py | 38 ++++++++----------- ncclient/operations/__init__.py | 24 +++++++++--- ncclient/operations/edit.py | 65 +++++++++++++------------------- ncclient/operations/errors.py | 14 +++++++ ncclient/operations/retrieve.py | 21 +++-------- ncclient/operations/rpc.py | 22 +++++------ ncclient/operations/session.py | 3 +- ncclient/operations/util.py | 34 ++++++++--------- ncclient/transport/hello.py | 5 +-- 11 files changed, 148 insertions(+), 166 deletions(-) diff --git a/ncclient/capabilities.py b/ncclient/capabilities.py index dba97c4..2e31ce8 100644 --- a/ncclient/capabilities.py +++ b/ncclient/capabilities.py @@ -20,7 +20,6 @@ class Capabilities: """ def __init__(self, capabilities=None): - "TODO: docstring" self._dict = {} if isinstance(capabilities, dict): self._dict = capabilities @@ -29,22 +28,18 @@ class Capabilities: self._dict[uri] = Capabilities.guess_shorthand(uri) def __contains__(self, key): - "TODO: docstring" return ( key in self._dict ) or ( key in self._dict.values() ) def __iter__(self): - "TODO: docstring" return self._dict.keys().__iter__() def __repr__(self): - "TODO: docstring" return repr(self._dict.keys()) def __list__(self): return self._dict.keys() def add(self, uri, shorthand=None): - "TODO: docstring" if shorthand is None: shorthand = Capabilities.guess_shorthand(uri) self._dict[uri] = shorthand @@ -52,7 +47,6 @@ class Capabilities: set = add def remove(self, key): - "TODO: docstring" if key in self._dict: del self._dict[key] else: @@ -63,7 +57,6 @@ class Capabilities: @staticmethod def guess_shorthand(uri): - "TODO: docstring" if uri.startswith('urn:ietf:params:netconf:capability:'): return (':' + uri.split(':')[5]) @@ -74,7 +67,7 @@ CAPABILITIES = Capabilities([ 'urn:ietf:params:netconf:capability:confirmed-commit:1.0', 'urn:ietf:params:netconf:capability:rollback-on-error:1.0', 'urn:ietf:params:netconf:capability:startup:1.0', - 'urn:ietf:params:netconf:capability:url:1.0', + 'urn:ietf:params:netconf:capability:url:1.0?scheme=http,ftp,file', 'urn:ietf:params:netconf:capability:validate:1.0', 'urn:ietf:params:netconf:capability:xpath:1.0', 'urn:ietf:params:netconf:capability:notification:1.0', diff --git a/ncclient/content.py b/ncclient/content.py index 462ada4..a895af1 100644 --- a/ncclient/content.py +++ b/ncclient/content.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"TODO: docstring" - from xml.etree import cElementTree as ET from ncclient import NCClientError @@ -44,43 +42,6 @@ multiqualify = lambda tag, nslist=(BASE_NS, CISCO_BS): [qualify(tag, ns) for ns unqualify = lambda tag: tag[tag.rfind('}')+1:] -### Other utility functions - -iselement = ET.iselement - -def namespaced_find(ele, tag, strict=False): - """In strict mode, doesn't work around Cisco implementations sending incorrectly - namespaced XML. Supply qualified name if using strict mode. - """ - found = None - if strict: - found = ele.find(tag) - else: - for qname in multiqualify(tag): - found = ele.find(qname) - if found is not None: - break - return found - -def parse_root(raw): - '''Parse the top-level element from XML string. - - Returns a `(tag, attributes)` tuple, where `tag` is a string representing - the qualified name of the root element and `attributes` is an - `{attribute: value}` dictionary. - ''' - fp = StringIO(raw[:1024]) # this is a guess but start element beyond 1024 bytes would be a bit absurd - for event, element in ET.iterparse(fp, events=('start',)): - return (element.tag, element.attrib) - -def root_ensured(rep, req_tag, req_attrs=None): - rep = to_element(rep) - if rep.tag not in (req_tag, qualify(req_tag)): - raise ContentError("Required root element [%s] not found" % req_tag) - if req_attrs is not None: - pass # TODO - return rep - ### XML with Python data structures dtree2ele = DictTree.Element @@ -146,3 +107,43 @@ class XML: @staticmethod def Element(xml): return ET.fromstring(xml) + +### Other utility functions + +iselement = ET.iselement + +def find(ele, tag, strict=False): + """In strict mode, doesn't workaround Cisco implementations sending incorrectly + namespaced XML. Supply qualified tag name if using strict mode. + """ + if strict: + return ele.find(tag) + else: + for qname in multiqualify(tag): + found = ele.find(qname) + if found is not None: + return found + +def parse_root(raw): + '''Parse the top-level element from XML string. + + Returns a `(tag, attributes)` tuple, where `tag` is a string representing + the qualified name of the root element and `attributes` is an + `{attribute: value}` dictionary. + ''' + fp = StringIO(raw[:1024]) # this is a guess but start element beyond 1024 bytes would be a bit absurd + for event, element in ET.iterparse(fp, events=('start',)): + return (element.tag, element.attrib) + +def validated_element(rep, tag, attrs=None): + ele = dtree2ele(rep) + if ele.tag not in (tag, qualify(tag)): + raise ContentError("Required root element [%s] not found" % tag) + if attrs is not None: + for req in attrs: + for attr in ele.attrib: + if unqualify(attr) == req: + break + else: + raise ContentError("Required attribute [%s] not found in element [%s]" % (req, req_tag)) + return ele diff --git a/ncclient/manager.py b/ncclient/manager.py index 87b8113..1459f67 100644 --- a/ncclient/manager.py +++ b/ncclient/manager.py @@ -41,17 +41,15 @@ connect = connect_ssh # default session type class Manager: - "Thin layer of abstraction for the ncclient API." + "Thin layer of abstraction for the API." - RAISE_ALL = 0 - RAISE_ERROR = 1 - RAISE_NONE = 2 + RAISE_ALL, RAISE_ERROR, RAISE_NONE = range(3) - def __init__(self, session, rpc_error=Manager.RAISE_ERROR): + def __init__(self, session, rpc_errors=Manager.RAISE_ALL): self._session = session self._raise = rpc_error - def rpc(self, op, *args, **kwds): + def do(self, op, *args, **kwds): op = OPERATIONS[op](self._session) reply = op.request(*args, **kwds) if not reply.ok: @@ -70,18 +68,15 @@ class Manager: self.close() return False - def _get(self, type, *args, **kwds): - reply = self.do(type) - return reply.data - def locked(self, target): - "Returns a context manager for use withthe 'with' statement. - `target` is the datastore to lock, e.g. 'candidate'" + """Returns a context manager for use withthe 'with' statement. + `target` is the datastore to lock, e.g. 'candidate + """ return operations.LockContext(self._session, target) - get = lambda self, *args, **kwds: self._get('get') + get = lambda self, *args, **kwds: self.do('get', *args, **kwds).data - get_config = lambda self, *args, **kwds: self._get('get-config') + get_config = lambda self, *args, **kwds: self.do('get-config', *args, **kwds).data edit_config = lambda self, *args, **kwds: self.do('edit-config', *args, **kwds) @@ -110,18 +105,17 @@ class Manager: pass if self._session.connected: # if that didn't work... self._session.close() - + @property def session(self, session): - return 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 - - + if whose in ('manager', 'client'): + return self._session._client_capabilities + elif whose in ('agent', 'server'): + return self._session._server_capabilities + @property def capabilities(self): return self._session._client_capabilities diff --git a/ncclient/operations/__init__.py b/ncclient/operations/__init__.py index 8e7c710..f8cedd6 100644 --- a/ncclient/operations/__init__.py +++ b/ncclient/operations/__init__.py @@ -14,20 +14,32 @@ 'NETCONF protocol operations' -from ncclient import NCClientError - -from rpc import RPC, RPCError -from errors import MissingCapabilityError +from errors import OperationError, MissingCapabilityError +from rpc import RPCError from retrieve import Get, GetConfig from edit import EditConfig, CopyConfig, DeleteConfig, Validate, Commit, DiscardChanges from session import CloseSession, KillSession from lock import Lock, Unlock, LockContext from subscribe import CreateSubscription +OPERATIONS = { + 'get': Get, + 'get-config': GetConfig, + 'edit-config': EditConfig, + 'copy-config': CopyConfig, + 'validate': Validate, + 'commit': Commit, + 'discard-changes': DiscardChanges, + 'delete-config': DeleteConfig, + 'lock': Lock, + 'unlock': Unlock, + 'close_session': CloseSession, + 'kill-session': KillSession, +} + __all__ = [ - 'RPC', - 'RPCReply', 'RPCError', + 'OPERATIONS', 'Get', 'GetConfig', 'EditConfig', diff --git a/ncclient/operations/edit.py b/ncclient/operations/edit.py index 538e419..4651c6e 100644 --- a/ncclient/operations/edit.py +++ b/ncclient/operations/edit.py @@ -27,16 +27,13 @@ class EditConfig(RPC): SPEC = {'tag': 'edit-config', 'subtree': []} - def request(self, target=None, target_url=None, config=None, - default_operation=None, test_option=None, error_option=None): - util.one_of(target, target_url) + def request(self, target=None, config=None, default_operation=None, + test_option=None, error_option=None): + util.one_of(target, config) spec = EditConfig.SPEC.copy() subtree = spec['subtree'] - subtree.append({ - 'tag': 'target', - 'subtree': util.store_or_url(target, target_url, self._assert) - }) - subtree.append(content.root_ensured(config, 'config')) + subtree.append(util.store_or_url('target', target, self._assert)) + subtree.append(content.validated_root(config, 'config')) if default_operation is not None: subtree.append({ 'tag': 'default-operation', @@ -64,12 +61,9 @@ class DeleteConfig(RPC): SPEC = {'tag': 'delete-config', 'subtree': []} - def request(self, target=None, target_url=None): + def request(self, target): spec = DeleteConfig.SPEC.copy() - spec['subtree'].append({ - 'tag': 'target', - 'subtree': util.store_or_url(target, target_url, self._assert) - }) + spec['subtree'].append(util.store_or_url('source', source, self._assert)) return self._request(spec) @@ -80,16 +74,10 @@ class CopyConfig(RPC): SPEC = {'tag': 'copy-config', 'subtree': []} - def request(self, source=None, source_url=None, target=None, target_url=None): + def request(self, source, target): spec = CopyConfig.SPEC.copy() - spec['subtree'].append({ - 'tag': 'source', - 'subtree': util.store_or_url(source, source_url, self._assert) - }) - spec['subtree'].append({ - 'tag': 'target', - 'subtree': util.store_or_url(target, target_url, self._assert) - }) + spec['subtree'].append(util.store_or_url('source', source, self._assert)) + spec['subtree'].append(util.store_or_url('target', source, self._assert)) return self._request(spec) @@ -104,16 +92,13 @@ class Validate(RPC): SPEC = {'tag': 'validate', 'subtree': []} - def request(self, source=None, source_url=None, config=None): - util.one_of(source, source_url, config) + def request(self, source=None, config=None): + util.one_of(source, config) spec = Validate.SPEC.copy() if config is None: - spec['subtree'].append({ - 'tag': 'source', - 'subtree': util.store_or_url(source, source_url, self._assert) - }) + spec['subtree'].append(util.store_or_url('source', source, self._assert)) else: - spec['subtree'].append(content.root_ensured(config, 'config')) + spec['subtree'].append(content.validated_root(config, 'config')) return self._request(spec) @@ -142,6 +127,16 @@ class Commit(RPC): return self._request(Commit.SPEC) +class DiscardChanges(RPC): + + # tested: no + # combed: yes + + DEPENDS = [':candidate'] + + SPEC = {'tag': 'discard-changes'} + + class ConfirmedCommit(Commit): "psuedo-op" @@ -157,14 +152,6 @@ class ConfirmedCommit(Commit): def confirm(self): "Make the confirming commit" return Commit.request(self, confirmed=True) - - -class DiscardChanges(RPC): - - # tested: no - # combed: yes - - DEPENDS = [':candidate'] - SPEC = {'tag': 'discard-changes'} - + def discard(self): + return DiscardChanges(self.session, self.async, self.timeout).request() diff --git a/ncclient/operations/errors.py b/ncclient/operations/errors.py index 2c1cf5e..836fcd0 100644 --- a/ncclient/operations/errors.py +++ b/ncclient/operations/errors.py @@ -1,3 +1,17 @@ +# Copyright 2009 Shikhar Bhushan +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from ncclient import NCClientError class OperationError(NCClientError): diff --git a/ncclient/operations/retrieve.py b/ncclient/operations/retrieve.py index 76b11d4..9e4c782 100644 --- a/ncclient/operations/retrieve.py +++ b/ncclient/operations/retrieve.py @@ -28,19 +28,13 @@ class GetReply(RPCReply): def _parsing_hook(self, root): self._data = None if not self._errors: - self._data = content.namespaced_find(root, 'data') + self._data = content.find(root, 'data') @property - def data_element(self): + def data(self): if not self._parsed: self.parse() return self._data - - @property - def data_xml(self): - return content.element2string(self.data_element) - - data = data_element class Get(RPC): @@ -57,9 +51,10 @@ class Get(RPC): def request(self, filter=None): spec = Get.SPEC.copy() if filter is not None: - spec['subtree'].append(util.build_filter(filter))) + spec['subtree'].append(util.build_filter(filter)) return self._request(spec) + class GetConfig(RPC): # tested: no @@ -72,18 +67,14 @@ class GetConfig(RPC): REPLY_CLS = GetReply - def request(self, source=None, source_url=None, filter=None): + 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({ - 'tag': 'source', - 'subtree': util.store_or_url(source, source_url) - }) + spec['subtree'].append(util.store_or_url('source', source, self._assert)) if filter is not None: spec['subtree'].append(util.build_filter(filter)) return self._request(spec) - diff --git a/ncclient/operations/rpc.py b/ncclient/operations/rpc.py index 05bdd1b..499902f 100644 --- a/ncclient/operations/rpc.py +++ b/ncclient/operations/rpc.py @@ -18,7 +18,7 @@ from weakref import WeakValueDictionary from ncclient import content -from reply import RPCReply +from errors import OperationError import logging logger = logging.getLogger('ncclient.rpc') @@ -129,11 +129,11 @@ class RPCReply: return root = self._root = content.xml2ele(self._raw) # element # per rfc 4741 an tag is sent when there are no errors or warnings - ok = content.namespaced_find(root, 'ok') + ok = content.find(root, 'ok') if ok is not None: logger.debug('parsed [%s]' % ok.tag) else: # create RPCError objects from elements - error = content.namespaced_find(root, 'rpc-error') + error = content.find(root, 'rpc-error') if error is not None: logger.debug('parsed [%s]' % error.tag) for err in root.getiterator(error.tag): @@ -141,8 +141,10 @@ class RPCReply: d = {} for err_detail in err.getchildren(): # etc.. tag = content.unqualify(err_detail.tag) - d[tag] = (err_detail.text.strip() if tag != 'error-info' - else content.ele2string(err_detail, 'utf-8')) + if tag != 'error-info': + d[tag] = err_detail.text.strip() + else: + d[tag] = content.ele2xml(err_detail) self._errors.append(RPCError(d)) self._parsing_hook(root) self._parsed = True @@ -175,18 +177,14 @@ class RPCReply: return self._errors -class RPCError(ncclient.RPCError): # raise it if you like +class RPCError(OperationError): # raise it if you like def __init__(self, err_dict): self._dict = err_dict if self.message is not None: - ncclient.RPCError.__init__(self, self.message) + OperationError.__init__(self, self.message) else: - ncclient.RPCError.__init__(self) - - @property - def raw(self): - return self._element.tostring() + OperationError.__init__(self) @property def type(self): diff --git a/ncclient/operations/session.py b/ncclient/operations/session.py index 47ec5f8..e4116f9 100644 --- a/ncclient/operations/session.py +++ b/ncclient/operations/session.py @@ -22,7 +22,7 @@ class CloseSession(RPC): SPEC = { 'tag': 'close-session' } - def _delivery_hook(self) + def _delivery_hook(self): self.session.close() @@ -44,4 +44,3 @@ class KillSession(RPC): 'text': session_id }) return self._request(spec) - diff --git a/ncclient/operations/util.py b/ncclient/operations/util.py index a12f633..2ca923e 100644 --- a/ncclient/operations/util.py +++ b/ncclient/operations/util.py @@ -14,11 +14,9 @@ 'Boilerplate ugliness' -from ncclient import OperationError -from ncclient.content import qualify as _ -from ncclient.content import root_ensured +from ncclient import content -from errors import MissingCapabilityError +from errors import OperationError, MissingCapabilityError def one_of(*args): 'Verifies that only one of the arguments is not None' @@ -31,16 +29,15 @@ def one_of(*args): return raise OperationError('Insufficient parameters') -def store_or_url(store, url, capcheck=None): - one_of(store, url) - node = {} - if store is not None: - node['tag'] = store - else: +def store_or_url(wha, loc, capcheck=None): + node = { 'tag': wha, 'subtree': {} } + if '://' in loc: # e.g. http://, file://, ftp:// if capcheck is not None: - capcheck(':url') # hmm.. schema check? deem overkill for now - node['tag'] = 'url' - node['text'] = url + capcheck(':url') # url schema check at some point! + node['subtree']['tag'] = 'url' + node['subtree']['text'] = loc + else: + node['subtree']['tag'] = loc return node def build_filter(spec, capcheck=None): @@ -51,14 +48,13 @@ def build_filter(spec, capcheck=None): 'tag': 'filter', 'attributes': {'type': type}, 'subtree': criteria - } + } else: - rep = root_ensure(spec, 'filter', 'type') + rep = content.validated_element(spec, 'filter', 'type') try: type = rep['type'] except KeyError: - type = ele[qualify('type')) - if type == 'xpath' and capcheck_func is not None: - capcheck_func(':xpath') + type = ele[content.qualify('type')] + if type == 'xpath' and capcheck is not None: + capcheck(':xpath') return rep - diff --git a/ncclient/transport/hello.py b/ncclient/transport/hello.py index 696c262..c30d1d1 100644 --- a/ncclient/transport/hello.py +++ b/ncclient/transport/hello.py @@ -16,15 +16,12 @@ from ncclient import content -class HelloHandler(Listener): +class HelloHandler: def __init__(self, init_cb, error_cb): self._init_cb = init_cb self._error_cb = error_cb - def __str__(self): - return 'HelloListener' - def callback(self, root, raw): if content.unqualify(root[0]) == 'hello': try: -- 1.7.10.4