moving forward
authorShikhar Bhushan <shikhar@schmizz.net>
Tue, 5 May 2009 14:21:18 +0000 (14:21 +0000)
committerShikhar Bhushan <shikhar@schmizz.net>
Tue, 5 May 2009 14:21:18 +0000 (14:21 +0000)
git-svn-id: http://ncclient.googlecode.com/svn/trunk@96 6dbcf712-26ac-11de-a2f3-1373824ab735

15 files changed:
ncclient/__init__.py
ncclient/capabilities.py
ncclient/content.py
ncclient/manager.py [new file with mode: 0644]
ncclient/operations/edit.py
ncclient/operations/lock.py
ncclient/operations/notification.py
ncclient/operations/retrieve.py
ncclient/operations/session.py
ncclient/rpc/__init__.py [new file with mode: 0644]
ncclient/rpc/listener.py [new file with mode: 0644]
ncclient/rpc/reply.py
ncclient/rpc/rpc.py
ncclient/transport/session.py
ncclient/transport/ssh.py

index de5eba7..9675ad1 100644 (file)
@@ -28,5 +28,8 @@ class TransportError(NCClientError):
 class OperationError(NCClientError):
     pass
 
+class OperationError(NCClientError):
+    pass
+
 class ContentError(NCClientError):
     pass
index 42971d4..12c7a97 100644 (file)
@@ -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:
index 5c87014..4d094b3 100644 (file)
@@ -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('<?xml'):
-            return '<?xml version="1.0" encoding="%s"?>%s' % (encoding, xml)
+            return ((u'<?xml version="1.0" encoding="%s"?>'
+                     % 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 (file)
index 0000000..bdc81f6
--- /dev/null
@@ -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
index f27f87d..d923d5d 100644 (file)
@@ -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)
index e9c337c..533509e 100644 (file)
 
 'Locking-related NETCONF operations'
 
-# TODO - a context manager around some <target> would be real neat
-
-from rpc import RPC
 from copy import deepcopy
 
+from ncclient.rpc import RPC
+
+# TODO - a context manager around some <target> would be real neat
+
 class Lock(RPC):
     
     SPEC = {
index f58fdbe..c4c82fb 100644 (file)
 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 = {
index 0d0f471..7540b11 100644 (file)
 # 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)
index 9d4674b..423e4ed 100644 (file)
@@ -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 (file)
index 0000000..1b4a467
--- /dev/null
@@ -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 (file)
index 0000000..965bf85
--- /dev/null
@@ -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('<rpc-reply> 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)
index 49c4ac6..4ea4621 100644 (file)
@@ -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) # <rpc-reply> element
+        root = self._root = ET.fromstring(self._raw) # <rpc-reply> 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
index 633e85f..050a99c 100644 (file)
@@ -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 <rpc-reply> 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 = '<?xml version="1.0" encoding="%s"?>' % encoding
-        doc = (u'<rpc message-id="%s" xmlns="%s">%s</rpc>' %
-               (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('<rpc-reply> 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)
index 05e74e1..667a85e 100644 (file)
@@ -88,3 +88,7 @@ class Session(Subject):
     @property
     def id(self):
         return self._id
+    
+    @property
+    def can_pipeline(self):
+        return True
index c633ca9..eef0421 100644 (file)
@@ -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