From: Shikhar Bhushan Date: Tue, 5 May 2009 14:21:18 +0000 (+0000) Subject: moving forward X-Git-Tag: 0.2a~95 X-Git-Url: https://code.grnet.gr/git/ncclient/commitdiff_plain/94803aaf6c9182724d23cecad57faeef418d4d92?hp=29cd1a5855c1a90ad31b5591b5e6d392b08b35d1 moving forward git-svn-id: http://ncclient.googlecode.com/svn/trunk@96 6dbcf712-26ac-11de-a2f3-1373824ab735 --- diff --git a/ncclient/__init__.py b/ncclient/__init__.py index de5eba7..9675ad1 100644 --- a/ncclient/__init__.py +++ b/ncclient/__init__.py @@ -28,5 +28,8 @@ class TransportError(NCClientError): class OperationError(NCClientError): pass +class OperationError(NCClientError): + pass + class ContentError(NCClientError): pass diff --git a/ncclient/capabilities.py b/ncclient/capabilities.py index 42971d4..12c7a97 100644 --- a/ncclient/capabilities.py +++ b/ncclient/capabilities.py @@ -40,6 +40,9 @@ class Capabilities: "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: diff --git a/ncclient/content.py b/ncclient/content.py index 5c87014..4d094b3 100644 --- a/ncclient/content.py +++ b/ncclient/content.py @@ -20,7 +20,6 @@ from xml.etree import cElementTree as ET ### Namespace-related ### BASE_NS = 'urn:ietf:params:xml:ns:netconf:base:1.0' -NOTIFICATION_NS = 'urn:ietf:params:xml:ns:netconf:notification:1.0' # and this is BASE_NS according to cisco devices... CISCO_BS = 'urn:ietf:params:netconf:base:1.0' @@ -43,25 +42,25 @@ multiqualify = lambda tag, nslist=(BASE_NS, CISCO_BS): [qualify(tag, ns) unqualify = lambda tag: tag[tag.rfind('}')+1:] - ### Build XML using Python data structures ### -class TreeBuilder: +class XMLConverter: """Build an ElementTree.Element instance from an XML tree specification based on nested dictionaries. TODO: describe spec """ def __init__(self, spec): "TODO: docstring" - self._root = TreeBuilder.build(spec) + self._root = XMLConverter.build(spec) def to_string(self, encoding='utf-8'): "TODO: docstring" xml = ET.tostring(self._root, encoding) - # some etree versions don't always include xml decl + # some etree versions don't always include xml decl e.g. with utf-8 # this is a problem with some devices if not xml.startswith('%s' % (encoding, xml) + return ((u'' + % encoding).encode(encoding) + xml) else: return xml @@ -82,18 +81,9 @@ class TreeBuilder: for child in children: ele.append(TreeBuilder.build(child)) return ele + elif 'xml' in spec: + return ET.XML(spec['xml']) elif 'comment' in spec: return ET.Comment(spec.get('comment')) else: raise ValueError('Invalid tree spec') - -class Parser: - pass - -class PartialParser(Parser): - - pass - -class RootParser(Parser): - - pass diff --git a/ncclient/manager.py b/ncclient/manager.py new file mode 100644 index 0000000..bdc81f6 --- /dev/null +++ b/ncclient/manager.py @@ -0,0 +1,50 @@ +# 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. + +import capabilities +import operations +import transport + +SESSION_TYPES = { + 'ssh': transport.SSHSession +} + +OPERATIONS = { + 'get': operations.Get, + 'get-config': operations.GetConfig, + 'edit-config': operations.EditConfig, + 'copy-config': operations.CopyConfig, + 'validate': operations.Validate, + 'commit': operations.Commit, + 'discard-changes': operations.DiscardChanges, + 'delete-config': operations.DeleteConfig, + 'lock': operations.Lock, + 'unlock': operations.Unlock, + 'close_session': operations.CloseSession, + 'kill-session': operations.KillSession, +} + +class Manager(type): + + 'Facade for the API' + + def connect(self, session_type, *args, **kwds): + self._session = SESSION_TYPES[session_type](capabilities.CAPABILITIES) + self._session.connect(*args, **kwds) + + def __getattr__(self, name): + if name in OPERATIONS: + return OPERATIONS[name](self._session).request + else: + raise AttributeError diff --git a/ncclient/operations/edit.py b/ncclient/operations/edit.py index f27f87d..d923d5d 100644 --- a/ncclient/operations/edit.py +++ b/ncclient/operations/edit.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rpc import RPC +from ncclient.rpc import RPC # TODO @@ -25,19 +25,61 @@ notes ''' class EditConfig(RPC): - pass + + SPEC = { + 'tag': 'edit-config', + 'children': [ + { 'target': None } + ] + } + + def request(self): + pass class CopyConfig(RPC): - pass + + SPEC = { + + } + + def request(self): + pass class DeleteConfig(RPC): - pass + + SPEC = { + 'tag': 'delete-config', + 'children': [ + 'tag': 'target', + 'children': {'tag': None } + ] + } + + def request(self, target=None, targeturl=None): + spec = deepcopy(DeleteConfig.SPEC) + class Validate(RPC): - pass + + DEPENDS = ['urn:ietf:params:netconf:capability:validate:1.0'] + SPEC = {} + + def request(self): + pass + class Commit(RPC): - pass # .confirm() ! + + SPEC = {'tag': 'commit'} + + def request(self): + return self._request(Commit.SPEC) + class DiscardChanges(RPC): - pass + + DEPENDS = ['urn:ietf:params:netconf:capability:candidate:1.0'] + SPEC = {'tag': 'discard-changes'} + + def request(self): + return self._request(DiscardChanges.SPEC) diff --git a/ncclient/operations/lock.py b/ncclient/operations/lock.py index e9c337c..533509e 100644 --- a/ncclient/operations/lock.py +++ b/ncclient/operations/lock.py @@ -14,11 +14,12 @@ 'Locking-related NETCONF operations' -# TODO - a context manager around some would be real neat - -from rpc import RPC from copy import deepcopy +from ncclient.rpc import RPC + +# TODO - a context manager around some would be real neat + class Lock(RPC): SPEC = { diff --git a/ncclient/operations/notification.py b/ncclient/operations/notification.py index f58fdbe..c4c82fb 100644 --- a/ncclient/operations/notification.py +++ b/ncclient/operations/notification.py @@ -17,9 +17,10 @@ from rpc import RPC from ncclient.glue import Listener -from ncclient.content import NOTIFICATION_NS from ncclient.content import qualify as _ +NOTIFICATION_NS = 'urn:ietf:params:xml:ns:netconf:notification:1.0' + class CreateSubscription(RPC): SPEC = { diff --git a/ncclient/operations/retrieve.py b/ncclient/operations/retrieve.py index 0d0f471..7540b11 100644 --- a/ncclient/operations/retrieve.py +++ b/ncclient/operations/retrieve.py @@ -12,13 +12,47 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rpc import RPC +from copy import deepcopy + +from ncclient.rpc import RPC + +def build_filter(spec, type, criteria): + filter = { + 'tag': 'filter', + 'attributes': {'type': type} + } + if type=='subtree': + if isinstance(criteria, dict): + filter['children'] = [criteria] + else: + filter['text'] = criteria + elif type=='xpath': + filter['attributes']['select'] = criteria class Get(RPC): + SPEC = { 'tag': 'get', - 'children': None + 'children': [] } + + def request(self, filter=None): + spec = deepcopy(SPEC) + if filter is not None: + spec['children'].append(build_filter(*filter)) + return self._request(spec) + class GetConfig(RPC): - pass + + SPEC = { + 'tag': 'get-config', + 'children': [ { 'tag': 'source', 'children': {'tag': None } } ] + } + + def request(self, source='running', filter=None): + spec = deepcopy(SPEC) + spec['children'][0]['children']['tag'] = source + if filter is not None: + spec['children'].append(build_filter(*filter)) + return self._request(spec) diff --git a/ncclient/operations/session.py b/ncclient/operations/session.py index 9d4674b..423e4ed 100644 --- a/ncclient/operations/session.py +++ b/ncclient/operations/session.py @@ -14,7 +14,7 @@ 'Session-related NETCONF operations' -from rpc import RPC +from ncclient.rpc import RPC from copy import deepcopy class CloseSession(RPC): diff --git a/ncclient/rpc/__init__.py b/ncclient/rpc/__init__.py new file mode 100644 index 0000000..1b4a467 --- /dev/null +++ b/ncclient/rpc/__init__.py @@ -0,0 +1,25 @@ +# 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 rpc import RPC +from reply import RPCReply + +class ReplyTimeoutError(Exception): + pass + +__all__ = [ + 'RPC', + 'RPCReply', + 'ReplyTimeoutError' +] diff --git a/ncclient/rpc/listener.py b/ncclient/rpc/listener.py new file mode 100644 index 0000000..965bf85 --- /dev/null +++ b/ncclient/rpc/listener.py @@ -0,0 +1,71 @@ +# 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 threading import Lock +from weakref import WeakValueDictionary + +import logging +logger = logging.getLogger('ncclient.rpc.listener') + +class RPCReplyListener(Listener): + + # one instance per session + def __new__(cls, session): + instance = session.get_listener_instance(cls) + if instance is None: + instance = object.__new__(cls) + instance._lock = Lock() + instance._id2rpc = WeakValueDictionary() + instance._pipelined = session.can_pipeline + instance._errback = None + session.add_listener(instance) + return instance + + def register(self, id, rpc): + with self._lock: + self._id2rpc[id] = rpc + + def callback(self, root, raw): + tag, attrs = root + if __(tag) != 'rpc-reply': + return + rpc = None + for key in attrs: + if __(key) == 'message-id': + id = attrs[key] + try: + with self._lock: + rpc = self._id2rpc.pop(id) + except KeyError: + logger.warning('no object registered for message-id: [%s]' % id) + except Exception as e: + logger.debug('error - %r' % e) + break + else: + if not self._pipelined: + with self._lock: + assert(len(self._id2rpc) == 1) + rpc = self._id2rpc.values()[0] + self._id2rpc.clear() + else: + logger.warning(' without message-id received: %s' % raw) + logger.debug('delivering to %r' % rpc) + rpc.deliver(raw) + + def set_errback(self, errback): + self._errback = errback + + def errback(self, err): + if self._errback is not None: + self._errback(err) diff --git a/ncclient/rpc/reply.py b/ncclient/rpc/reply.py index 49c4ac6..4ea4621 100644 --- a/ncclient/rpc/reply.py +++ b/ncclient/rpc/reply.py @@ -18,13 +18,14 @@ from ncclient.content import multiqualify as _ from ncclient.content import unqualify as __ import logging -logger = logging.getLogger('ncclient.operations.reply') +logger = logging.getLogger('ncclient.rpc.reply') class RPCReply: def __init__(self, raw): self._raw = raw self._parsed = False + self._root = None self._errors = [] def __repr__(self): @@ -32,7 +33,7 @@ class RPCReply: def parse(self): if self._parsed: return - root = ET.fromstring(self._raw) # element + root = self._root = ET.fromstring(self._raw) # element if __(root.tag) != 'rpc-reply': raise ValueError('Root element is not RPC reply') @@ -66,6 +67,10 @@ class RPCReply: return self._raw @property + def root(self): + return self._root + + @property def ok(self): if not self._parsed: self.parse() return not self._errors # empty list => false diff --git a/ncclient/rpc/rpc.py b/ncclient/rpc/rpc.py index 633e85f..050a99c 100644 --- a/ncclient/rpc/rpc.py +++ b/ncclient/rpc/rpc.py @@ -21,49 +21,48 @@ from ncclient.content import qualify as _ from ncclient.content import unqualify as __ from ncclient.glue import Listener -from . import logger +from listener import RPCReplyListener from reply import RPCReply +import logging +logger = logging.getLogger('ncclient.rpc') -# Cisco does not include message-id attribute in in case of an error. -# This is messed up however we have to deal with it. -# So essentially, there can be only one operation at a time if we are talking to -# a Cisco device. - -def cisco_check(session): - try: - return session.is_remote_cisco - except AttributeError: - return False class RPC(object): - def __init__(self, session, async=False): - if cisco_check(session) and async: - raise UserWarning('Asynchronous mode not supported for Cisco devices') + 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 self._async = async + self._timeout = timeout self._id = uuid1().urn self._listener = RPCReplyListener(session) self._listener.register(self._id, self) self._reply = None self._reply_event = Event() - def _build(self, op, encoding='utf-8'): - if isinstance(op, dict): - return self.build_from_spec(self._id, op, encoding) - else: - return self.build_from_string(self._id, op, encoding) + def _build(opspec, encoding='utf-8'): + "TODO: docstring" + spec = { + 'tag': _('rpc'), + 'attributes': {'message-id': self._id}, + 'children': opspec + } + return TreeBuilder(spec).to_string(encoding) - def _request(self, op): + def _request(self, op, timeout=None): req = self._build(op) self._session.send(req) if self._async: return self._reply_event else: - self._reply_event.wait() - self._reply.parse() - return self._reply + self._reply_event.wait(timeout) + if self._reply_event.isSet(): + self._reply.parse() + return self._reply + else: + raise ReplyTimeoutError def _delivery_hook(self): 'For subclasses' @@ -83,10 +82,6 @@ class RPC(object): return self._reply @property - def is_async(self): - return self._async - - @property def id(self): return self._id @@ -98,75 +93,8 @@ class RPC(object): def reply_event(self): return self._reply_event - @staticmethod - def build_from_spec(msgid, opspec, encoding='utf-8'): - "TODO: docstring" - spec = { - 'tag': _('rpc'), - 'attributes': {'message-id': msgid}, - 'children': opspec - } - return TreeBuilder(spec).to_string(encoding) - - @staticmethod - def build_from_string(msgid, opstr, encoding='utf-8'): - "TODO: docstring" - decl = '' % encoding - doc = (u'%s' % - (msgid, BASE_NS, opstr)).encode(encoding) - return '%s%s' % (decl, doc) - - -class RPCReplyListener(Listener): - - # TODO - determine if need locking - - # one instance per session - def __new__(cls, session): - instance = session.get_listener_instance(cls) - if instance is None: - instance = object.__new__(cls) - instance._id2rpc = WeakValueDictionary() - instance._cisco = cisco_check(session) - instance._errback = None - session.add_listener(instance) - return instance - - def __str__(self): - return 'RPCReplyListener' - - def set_errback(self, errback): - self._errback = errback - - def register(self, id, rpc): - self._id2rpc[id] = rpc - - def callback(self, root, raw): - tag, attrs = root - if __(tag) != 'rpc-reply': - return - rpc = None - for key in attrs: - if __(key) == 'message-id': - id = attrs[key] - try: - rpc = self._id2rpc.pop(id) - except KeyError: - logger.warning('[RPCReplyListener.callback] no object ' - + 'registered for message-id: [%s]' % id) - except Exception as e: - logger.debug('[RPCReplyListener.callback] error - %r' % e) - break - else: - if self._cisco: - assert(len(self._id2rpc) == 1) - rpc = self._id2rpc.values()[0] - self._id2rpc.clear() - else: - logger.warning(' without message-id received: %s' % raw) - logger.debug('[RPCReplyListener.callback] delivering to %r' % rpc) - rpc.deliver(raw) + def set_async(self, bool): self._async = bool + async = property(fget=lambda self: self._async, fset=set_async) - def errback(self, err): - if self._errback is not None: - self._errback(err) + def set_timeout(self, timeout): self._timeout = timeout + timeout = property(fget=lambda self: self._timeout, fset=set_timeout) diff --git a/ncclient/transport/session.py b/ncclient/transport/session.py index 05e74e1..667a85e 100644 --- a/ncclient/transport/session.py +++ b/ncclient/transport/session.py @@ -88,3 +88,7 @@ class Session(Subject): @property def id(self): return self._id + + @property + def can_pipeline(self): + return True diff --git a/ncclient/transport/ssh.py b/ncclient/transport/ssh.py index c633ca9..eef0421 100644 --- a/ncclient/transport/ssh.py +++ b/ncclient/transport/ssh.py @@ -289,5 +289,8 @@ class SSHSession(Session): return self._transport @property - def is_remote_cisco(self): - return 'Cisco' in self._transport.remote_version + def can_pipeline(self): + if 'Cisco' in self._transport.remote_version: + return False + # elif .. + return True