broken but too big a delta to not commit
authorShikhar Bhushan <shikhar@schmizz.net>
Wed, 6 May 2009 14:15:08 +0000 (14:15 +0000)
committerShikhar Bhushan <shikhar@schmizz.net>
Wed, 6 May 2009 14:15:08 +0000 (14:15 +0000)
git-svn-id: http://ncclient.googlecode.com/svn/trunk@97 6dbcf712-26ac-11de-a2f3-1373824ab735

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

index 9675ad1..d8e0543 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+'''
+NOTES
+=====
+
+- operations complete
+- make operational again
+- LockContext
+- op specfic reply objects
+- manager testing and augmenting
+parse into dicts??
+
+'''
+
 import sys
 
 if sys.version_info < (2, 6):
index 12c7a97..dba97c4 100644 (file)
@@ -67,17 +67,16 @@ class Capabilities:
         if uri.startswith('urn:ietf:params:netconf:capability:'):
             return (':' + uri.split(':')[5])
 
-
 CAPABILITIES = Capabilities([
-    'urn:ietf:params:netconf:base:1.0', # TODO
-    'urn:ietf:params:netconf:capability:writable-running:1.0', # TODO
-    'urn:ietf:params:netconf:capability:candidate:1.0', # TODO
-    'urn:ietf:params:netconf:capability:confirmed-commit:1.0', # TODO
-    'urn:ietf:params:netconf:capability:rollback-on-error:1.0', # TODO
-    'urn:ietf:params:netconf:capability:startup:1.0', # TODO
-    'urn:ietf:params:netconf:capability:url:1.0', # TODO
-    'urn:ietf:params:netconf:capability:validate:1.0', # TODO
-    'urn:ietf:params:netconf:capability:xpath:1.0', # TODO
-    'urn:ietf:params:netconf:capability:notification:1.0', # TODO
-    'urn:ietf:params:netconf:capability:interleave:1.0' # TODO
-    ])
+    'urn:ietf:params:netconf:base:1.0',
+    'urn:ietf:params:netconf:capability:writable-running:1.0',
+    'urn:ietf:params:netconf:capability:candidate:1.0',
+    '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:validate:1.0',
+    'urn:ietf:params:netconf:capability:xpath:1.0',
+    'urn:ietf:params:netconf:capability:notification:1.0',
+    'urn:ietf:params:netconf:capability:interleave:1.0'
+])
index 4d094b3..8bcb75c 100644 (file)
@@ -56,10 +56,10 @@ class XMLConverter:
     def to_string(self, encoding='utf-8'):
         "TODO: docstring"
         xml = ET.tostring(self._root, encoding)
-        # some etree versions don't always include xml decl e.g. with utf-8
+        # some etree versions don't include xml decl with utf-8
         # this is a problem with some devices
-        if not xml.startswith('<?xml'):
-            return ((u'<?xml version="1.0" encoding="%s"?>'
+        if encoding == 'utf-8':
+            return ((u'<?xml version="1.0" encoding="utf-8"?>'
                      % encoding).encode(encoding) + xml)
         else:
             return xml
@@ -72,17 +72,19 @@ class XMLConverter:
     @staticmethod
     def build(spec):
         "TODO: docstring"
-        if 'tag' in spec:
+        if ET.iselement(spec):
+            return spec
+        elif isinstance(spec, basestring):
+            return ET.XML(spec)
+        # assume isinstance(spec, dict)
+        elif 'tag' in spec:
             ele = ET.Element(spec.get('tag'), spec.get('attributes', {}))
-            ele.text = spec.get('text', '')
+            ele.text = str(spec.get('text', ''))
             children = spec.get('children', [])
-            if isinstance(children, dict):
-                children = [children]
+            if isinstance(children, dict): children = [children]
             for child in children:
-                ele.append(TreeBuilder.build(child))
+                ET.SubElement(ele, 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:
index d361118..deb57d1 100644 (file)
 "TODO: docstring"
 
 from cStringIO import StringIO
-from threading import Thread
-from Queue import Queue
-from threading import Lock
+from threading import Thread, Lock
 from xml.etree import cElementTree as ET
 
 import logging
 logger = logging.getLogger('ncclient.glue')
 
+
 def parse_root(raw):
     '''Parse the top-level element from a string representing an XML document.
     
@@ -30,7 +29,7 @@ def parse_root(raw):
     the qualified name of the root element and `attributes` is an
     `{attribute: value}` dictionary.
     '''
-    fp = StringIO(raw)
+    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)
 
@@ -42,13 +41,16 @@ class Subject(Thread):
     def __init__(self):
         "TODO: docstring"
         Thread.__init__(self)
-        self._q = Queue()
         self._listeners = set() # TODO(?) weakref
         self._lock = Lock()
     
     def _dispatch_message(self, raw):
         "TODO: docstring"
-        root = parse_root(raw)
+        try:
+            root = parse_root(raw)
+        except Exception as e:
+            logger.error('error parsing dispatch message: %s' % e)
+            return
         with self._lock:
             listeners = list(self._listeners)
         for l in listeners:
@@ -89,11 +91,6 @@ class Subject(Thread):
             for listener in self._listeners:
                 if isinstance(listener, cls):
                     return listener
-    
-    def send(self, message):
-        "TODO: docstring"
-        logger.debug('queueing %s' % message)
-        self._q.put(message)
 
 
 class Listener(object):
index bdc81f6..8dbc141 100644 (file)
@@ -44,7 +44,27 @@ class Manager(type):
         self._session.connect(*args, **kwds)
     
     def __getattr__(self, name):
+        name = name.replace('_', '-')
         if name in OPERATIONS:
             return OPERATIONS[name](self._session).request
         else:
             raise AttributeError
+    
+    def get(self, *args, **kwds):
+        g = operations.Get(self._session)
+        reply = g.request(*args, **kwds)
+        if reply.errors:
+            raise RPCError(reply.errors)
+        else:
+            return reply.data
+    
+    def get_config(self, *args, **kwds):
+        gc = operations.GetConfig(self._session)
+        reply = gc.request(*args, **kwds)
+        if reply.errors:
+            raise RPCError(reply.errors)
+        else:
+            return reply.data
+
+    def locked(self, target='running'):
+        return LockContext(self._session, target)
index d923d5d..1d66afb 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from ncclient.capabilities import URI
 from ncclient.rpc import RPC
 
-# TODO
-
-
-'''
-notes
--> editconfig and copyconfig <running> target depends on :writable-running
--> 
-
-'''
+import util
 
 class EditConfig(RPC):
     
@@ -36,49 +29,109 @@ class EditConfig(RPC):
     def request(self):
         pass
 
-class CopyConfig(RPC):
+
+class DeleteConfig(RPC): # x
     
     SPEC = {
-        
+        'tag': 'delete-config',
+        'children': [ { 'tag': 'target', 'children': None } ]
     }
     
-    def request(self):
-        pass
+    def request(self, target=None, target_url=None):
+        spec = DeleteConfig.SPEC.copy()
+        spec['children'][0]['children'] = util.store_or_url(target, target_url)
+        return self._request(spec)
+
 
-class DeleteConfig(RPC):
+class CopyConfig(RPC): # x
     
     SPEC = {
-        'tag': 'delete-config',
+        'tag': 'copy-config',
         'children': [
-            'tag': 'target',
-            'children': {'tag': None }
+            { 'tag': 'source', 'children': {'tag': None } },
+            { 'tag': 'target', 'children': {'tag': None } }
         ]
     }
     
-    def request(self, target=None, targeturl=None):
-        spec = deepcopy(DeleteConfig.SPEC)
-        
+    def request(self, source=None, source_url=None, target=None, target_url=None):
+        spec = CopyConfig.SPEC.copy()
+        spec['children'][0]['children'] = util.store_or_url(source, source_url)
+        spec['children'][1]['children'] = util.store_or_url(target, target_url)
+        return self._request(spec)
 
-class Validate(RPC):
+
+class Validate(RPC): # xxxxx
     
-    DEPENDS = ['urn:ietf:params:netconf:capability:validate:1.0']
-    SPEC = {}
+    DEPENDS = [':validate']
     
-    def request(self):
-        pass
-
+    SPEC = {
+        'tag': 'validate',
+        'children': []
+    }
+    
+    def request(self, source=None, config=None):
+        #self.either_or(source, config)
+        #
+        #if source is None and config is None:
+        #    raise OperationError('Insufficient parameters')
+        #if source is not None and config is not None:
+        #    raise OperationError('Too many parameters')
+        #spec = Validate.SPEC.copy()
+        #
+        util.one_of(source, capability)
+        if source is not None:
+            spec['children'].append({
+                'tag': 'source',
+                'children': {'tag': source}
+                })
+        #
+        #else:
+        #    if isinstance(config, dict):
+        #        if config['tag'] != 'config':
+        #            child['tag'] = 'config'
+        #            child['children'] = config
+        #        else:
+        #            child = config
+        #    elif isinstance(config, Element):
+        #        pass
+        #    else:
+        #        from xml.etree import cElementTree as ET
+        #        ele = ET.XML(unicode(config))
+        #        if __(ele.tag) != 'config':
+        #            pass
+        #        else:
+        #            pass
+        #    spec['children'].append(child)
+        #
+        return self._request(spec)
 
-class Commit(RPC):
+class Commit(RPC): # x
     
-    SPEC = {'tag': 'commit'}
+    DEPENDS = [':candidate']
     
-    def request(self):
+    SPEC = {'tag': 'commit', 'children': [] }
+    
+    def _parse_hook(self):
+        pass
+    
+    def request(self, confirmed=False, timeout=None):
+        spec = SPEC.copy()
+        if confirmed:
+            self._assert(':confirmed-commit')
+            children = spec['children']
+            children.append({'tag': 'confirmed'})
+            if timeout is not None:
+                children.append({
+                    'tag': 'confirm-timeout',
+                    'text': timeout
+                })
         return self._request(Commit.SPEC)
 
 
-class DiscardChanges(RPC):
+class DiscardChanges(RPC): # x
+    
+    DEPENDS = [':candidate']
     
-    DEPENDS = ['urn:ietf:params:netconf:capability:candidate:1.0']
     SPEC = {'tag': 'discard-changes'}
     
     def request(self):
index 533509e..d0d9c29 100644 (file)
@@ -20,7 +20,7 @@ from ncclient.rpc import RPC
 
 # TODO - a context manager around some <target> would be real neat
 
-class Lock(RPC):
+class Lock(RPC): # x
     
     SPEC = {
         'tag': 'lock',
@@ -31,12 +31,14 @@ class Lock(RPC):
     }
     
     def request(self, target='running'):
+        if target=='candidate':
+            self._assert(':candidate')
         spec = deepcopy(Lock.SPEC)
         spec['children']['children']['tag'] = target
         return self._request(spec)
 
 
-class Unlock(RPC):
+class Unlock(RPC): # x
     
     SPEC = {
         'tag': 'unlock',
@@ -47,6 +49,23 @@ class Unlock(RPC):
     }
     
     def request(self, target='running'):
+        if target=='candidate':
+            self._assert(':candidate')
         spec = deepcopy(Unlock.SPEC)
         spec['children']['children']['tag'] = target
         return self._request(self.spec)
+
+
+class LockContext:
+        
+    def __init__(self, session, target='running'):
+        self.session = session
+        self.target = target
+        
+    def __enter__(self):
+        Lock(self.session).request(self.target)
+        return self
+    
+    def __exit__(self, t, v, tb):
+        Unlock(self.session).request(self.target)
+        return False
index 7540b11..8c2e62a 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from copy import deepcopy
-
-from ncclient.rpc import RPC
+from ncclient.rpc import RPC, RPCReply
 
 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':
+    if type == 'subtree':
+        filter['children'] = [criteria]
+    elif type == 'xpath':
         filter['attributes']['select'] = criteria
+    return filter
 
-class Get(RPC):
+class Get(RPC): # xx
     
     SPEC = {
         'tag': 'get',
         'children': []
     }
     
+    REPLY_CLS = GetReply
+    
     def request(self, filter=None):
-        spec = deepcopy(SPEC)
+        spec = Get.SPEC.copy()
         if filter is not None:
+            #if filter[0] == 'xpath':
+            #    self._assert(':xpath')
             spec['children'].append(build_filter(*filter))
         return self._request(spec)
 
+class GetReply(RPCReply):
+    
+    def parse(self):
+        RPCReply.parse(self)
 
-class GetConfig(RPC):
+class GetConfig(RPC): # xx
     
     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
+    REPLY_CLS = GetConfigReply
+    
+    def request(self, source=None, source_url=None, filter=None):
+        self._one_of(source, source_url)
+        spec = GetConfig.SPEC.copy()
+        if source is not None:
+            spec['children'][0]['children']['tag'] = source
+        if source_url is not None:
+            #self._assert(':url')
+            spec['children'][0]['children']['tag'] = 'url'
+            spec['children'][0]['children']['text'] = source_url        
         if filter is not None:
+            #if filter[0] == 'xpath':
+            #    self._assert(':xpath')
             spec['children'].append(build_filter(*filter))
         return self._request(spec)
+
+class GetReply(RPCReply):
+    
+    def parse(self):
+        RPCReply.parse(self)
index 423e4ed..6087fdd 100644 (file)
@@ -15,9 +15,8 @@
 'Session-related NETCONF operations'
 
 from ncclient.rpc import RPC
-from copy import deepcopy
 
-class CloseSession(RPC):
+class CloseSession(RPC): # x
     
     'CloseSession is always synchronous'
     
@@ -32,16 +31,16 @@ class CloseSession(RPC):
         return self._request(CloseSession.SPEC)
 
 
-class KillSession(RPC):
+class KillSession(RPC): # x
     
     SPEC = {
         'tag': 'kill-session',
-        'children': [ { 'tag': 'session-id', 'text': None} ]
+        'children': { 'tag': 'session-id', 'text': None}
     }
     
     def request(self, session_id):
         if not isinstance(session_id, basestring): # just making sure...
             session_id = str(session_id)
-        spec = deepcopy(SPEC)
+        spec = KillSession.SPEC.copy()
         spec['children'][0]['text'] = session_id
         return self._request(spec)
diff --git a/ncclient/operations/util.py b/ncclient/operations/util.py
new file mode 100644 (file)
index 0000000..abe86a3
--- /dev/null
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+
+'boilerplate'
+
+from ncclient import OperationError
+
+class MissingCapabilityError(OperationError):
+    pass
+
+def one_of(self, *args):
+    for i, arg in enumerate(args):
+        if arg is not None:
+            for argh in args[i+1:]:
+                if argh is not None:
+                    raise OperationError('Too many parameters')
+            else:
+                return
+    raise OperationError('Insufficient parameters')
+
+
+def assert_capability(key, capabilities):
+    if key not in capabilities:
+        raise MissingCapabilityError
+
+
+def store_or_url(store, url):
+    one_of(store, url)
+    node = {}
+    if store is not None:
+        node['tag'] = store
+    else:
+        node['tag'] = 'url'
+        node['text'] = url
+    return node
index 1b4a467..b0de5ad 100644 (file)
@@ -15,8 +15,9 @@
 from rpc import RPC
 from reply import RPCReply
 
-class ReplyTimeoutError(Exception):
-    pass
+from ncclient import RPCError
+
+class ReplyTimeoutError(RPCError): pass
 
 __all__ = [
     'RPC',
index 4ea4621..47b9725 100644 (file)
@@ -38,27 +38,31 @@ class RPCReply:
         if __(root.tag) != 'rpc-reply':
             raise ValueError('Root element is not RPC reply')
         
+        ok = False
         # per rfc 4741 an <ok/> tag is sent when there are no errors or warnings
         oktags = _('ok')
         for oktag in oktags:
             if root.find(oktag) is not None:
                 logger.debug('parsed [%s]' % oktag)
-                self._parsed = True
-                return
-        
-        # create RPCError objects from <rpc-error> elements
-        errtags = _('rpc-error')
-        for errtag in errtags:
-            for err in root.getiterator(errtag): # a particular <rpc-error>
-                logger.debug('parsed [%s]' % errtag)
-                d = {}
-                for err_detail in err.getchildren(): # <error-type> etc..
-                    tag = __(err_detail.tag)
-                    d[tag] = (err_detail.text.strip() if tag != 'error-info'
-                              else ET.tostring(err_detail, 'utf-8'))
-                self._errors.append(RPCError(d))
-            if self._errors:
                 break
+        else:
+            # create RPCError objects from <rpc-error> elements
+            errtags = _('rpc-error')
+            for errtag in errtags:
+                for err in root.getiterator(errtag): # a particular <rpc-error>
+                    logger.debug('parsed [%s]' % errtag)
+                    d = {}
+                    for err_detail in err.getchildren(): # <error-type> etc..
+                        tag = __(err_detail.tag)
+                        d[tag] = (err_detail.text.strip() if tag != 'error-info'
+                                  else ET.tostring(err_detail, 'utf-8'))
+                    self._errors.append(RPCError(d))
+                if self._errors:
+                    break
+        
+        if self.ok:
+            # TODO: store children in some way...
+            pass
         
         self._parsed = True
     
index 050a99c..2c5c202 100644 (file)
@@ -27,13 +27,20 @@ from reply import RPCReply
 import logging
 logger = logging.getLogger('ncclient.rpc')
 
-
 class RPC(object):
     
+    DEPENDS = []
+    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:
+                self.assert_capability(cap)
+        except AttributeError:
+            pass        
         self._async = async
         self._timeout = timeout
         self._id = uuid1().urn
@@ -51,13 +58,13 @@ class RPC(object):
             }
         return TreeBuilder(spec).to_string(encoding)
     
-    def _request(self, op, timeout=None):
+    def _request(self, op):
         req = self._build(op)
         self._session.send(req)
         if self._async:
             return self._reply_event
         else:
-            self._reply_event.wait(timeout)
+            self._reply_event.wait(self._timeout)
             if self._reply_event.isSet():
                 self._reply.parse()
                 return self._reply
@@ -68,8 +75,12 @@ class RPC(object):
         'For subclasses'
         pass
     
+    def _assert(self, capability):
+        if capability not in self._session.server_capabilities:
+            raise MissingCapabilityError('Server does not support [%s]' % cap)
+    
     def deliver(self, raw):
-        self._reply = RPCReply(raw)
+        self._reply = self.REPLY_CLS(raw)
         self._delivery_hook()
         self._reply_event.set()
     
index 667a85e..56c57bb 100644 (file)
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 from threading import Event
+from Queue import Queue
 
 from ncclient.capabilities import Capabilities, CAPABILITIES
 from ncclient.glue import Subject
@@ -30,6 +31,7 @@ class Session(Subject):
         "Subclass constructor should call this"
         Subject.__init__(self)
         self.setName('session')
+        self._q = Queue()
         self._client_capabilities = CAPABILITIES
         self._server_capabilities = None # yet
         self._id = None # session-id
@@ -71,6 +73,11 @@ class Session(Subject):
         "Subclass implements"
         raise NotImplementedError
     
+    def send(self, message):
+        "TODO: docstring"
+        logger.debug('queueing %s' % message)
+        self._q.put(message)
+    
     ### Properties
     
     @property
index 4ac526a..548447e 100644 (file)
@@ -14,9 +14,6 @@
 
 from ncclient.glue import Listener
 
-import logging
-logger = logging.getLogger('PrintListener')
-
 class PrintListener(Listener):
     
     def callback(self, root, raw):