session layer is somewhat stable
authorShikhar Bhushan <shikhar@schmizz.net>
Mon, 20 Apr 2009 04:15:41 +0000 (04:15 +0000)
committerShikhar Bhushan <shikhar@schmizz.net>
Mon, 20 Apr 2009 04:15:41 +0000 (04:15 +0000)
git-svn-id: http://ncclient.googlecode.com/svn/trunk@27 6dbcf712-26ac-11de-a2f3-1373824ab735

ncclient/__init__.py [new file with mode: 0644]
ncclient/content/__init__.py [new file with mode: 0644]
ncclient/content/hello.py [new file with mode: 0644]
ncclient/rpc.py [new file with mode: 0644]
ncclient/session.py [new file with mode: 0644]
ncclient/ssh.py [new file with mode: 0644]

diff --git a/ncclient/__init__.py b/ncclient/__init__.py
new file mode 100644 (file)
index 0000000..b43f4a5
--- /dev/null
@@ -0,0 +1,15 @@
+# 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.
+
+__version__ = "0.01"
diff --git a/ncclient/content/__init__.py b/ncclient/content/__init__.py
new file mode 100644 (file)
index 0000000..de18804
--- /dev/null
@@ -0,0 +1,19 @@
+# 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 ..error import ClientError, NETCONFError
+
+class ContentError(ClientError): pass
+
+class ValidationError(NETCONFError): pass
diff --git a/ncclient/content/hello.py b/ncclient/content/hello.py
new file mode 100644 (file)
index 0000000..a846c34
--- /dev/null
@@ -0,0 +1,73 @@
+# 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 logging
+from xml.etree import cElementTree as ElementTree
+
+logging.getLogger('ncclient.content.hello')
+
+from ..capability import Capabilities
+
+ns = 'urn:ietf:params:xml:ns:netconf:base:1.0'
+
+def make(capabilities):
+    return '<hello xmlns="%s">%s</hello>' % (ns, capabilities)
+
+def parse(raw):
+    id, capabilities = 0, Capabilities()
+    hello = ElementTree.fromstring(raw)
+    for child in hello.getchildren():
+        if child.tag == '{%s}session-id' % ns:
+            id = child.text
+        elif child.tag == '{%s}capabilities' % ns:
+            for cap in child.getiterator('{%s}capability' % ns):
+                capabilities.add(cap.text)
+    return id, capabilities
+
+#class HelloParser:
+#    
+#    'Fast parsing with expat'
+#    
+#    capability, sid = range(2)
+#    
+#    def __init__(self, raw):
+#        self._sid = None
+#        self._capabilities = Capabilities()
+#        p = xml.parsers.expat.ParserCreate()
+#        p.StartElementHandler = self._start_element
+#        p.EndElementHandler = self._end_element
+#        p.CharacterDataHandler = self._char_data
+#        self._expect = None
+#        p.parse(raw, True)
+#    
+#    def _start_element(self, name, attrs):
+#        if name == 'capability':
+#            self._expect = HelloParser.capability
+#        elif name == 'session-id':
+#            self._expect = HelloParser.sid
+#    
+#    def _end_element(self, name):
+#        self._expect = None
+#    
+#    def _char_data(self, data):
+#        if self._expect == HelloParser.capability:
+#            self._capabilities.add(data)
+#        elif self._expect == HelloParser.sid:
+#            self._sid = int(data)
+#    
+#    @property
+#    def sid(self): return self._sid
+#    
+#    @property
+#    def capabilities(self): return self._capabilities
\ No newline at end of file
diff --git a/ncclient/rpc.py b/ncclient/rpc.py
new file mode 100644 (file)
index 0000000..c7d83bf
--- /dev/null
@@ -0,0 +1,78 @@
+# 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 Event
+
+from listener import Listener
+
+from content import MessageIDParser
+
+class RPC:
+    
+    cur_id = {}
+
+    def __init__(self, session=None, async=False):
+        self._session = None
+        self._async = None
+        self._reply = None
+        self._event = Event()
+    
+    def get_reply(self, timeout=2.0):
+        self._event.wait(timeout)
+        if self._event.isSet():
+            return self._reply
+    
+    def do(self, session, async=False):
+        self._async = async
+    
+    def deliver(self, reply):
+        self._reply = reply
+        self._event.set()
+
+    @property
+    def has_reply(self): return self._event.isSet()
+    
+    @property
+    def async(self): return self._async
+    
+    @property
+    def listener(self): return self._listener
+    
+    def _next_id(self):
+        cur_id[self._sid] = cur_id.get(self._sid, 0) + 1
+        return cur_id[self._sid]
+    
+class RPCReply:
+    
+    def __init__(self, raw):
+        self._raw = raw
+    
+    def get_id(self):
+        return content.rpc.parse_msg_id(raw)
+
+class RPCError(NETCONFError):
+    
+    pass
+
+class ReplyListener(Listener):
+    
+    def __init__(self):
+        self._id2rpc = {}
+    
+    def reply(self, msg):
+        reply = RPCReply(msg)
+        id2rpc[reply.get_id()].deliver(reply)
+    
+    def error(self, buf):
+        pass
diff --git a/ncclient/session.py b/ncclient/session.py
new file mode 100644 (file)
index 0000000..949f569
--- /dev/null
@@ -0,0 +1,108 @@
+# 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 logging
+
+from threading import Thread, Event
+from Queue import Queue
+
+from error import ClientError
+from content import hello
+from listener import Subject
+from capability import CAPABILITIES
+
+logger = logging.getLogger('ncclient.session')
+
+class SessionError(ClientError): pass
+
+class Session(Thread, Subject):
+    
+    def __init__(self):
+        Thread.__init__(self, name='session')
+        Subject.__init__(self, listeners=[Session.HelloListener(self)])
+        self._client_capabilities = CAPABILITIES
+        self._server_capabilities = None # yet
+        self._id = None # session-id
+        self._connected = False
+        self._error = None
+        self._init_event = Event()
+        self._q = Queue()
+    
+    def _close(self):
+        self._connected = False
+    
+    def _init(self):
+        self._connected = True
+        # start the subclass' main loop
+        self.start()
+        # queue client's hello message for sending
+        self.send(hello.make(self._client_capabilities))
+        # we expect server's hello message, wait for _init_event to be set by HelloListener
+        self._init_event.wait()
+        # there may have been an error
+        if self._error:
+            self._close()
+            raise self._error
+
+    def connect(self):
+        raise NotImplementedError
+
+    def send(self, message):
+        message = (u'<?xml version="1.0" encoding="UTF-8"?>%s' % message).encode('utf-8')
+        logger.debug('queueing message: \n%s' % message)
+        self._q.put(message)
+
+    def run(self):
+        raise NotImplementedError
+    
+    ### Properties
+
+    @property
+    def client_capabilities(self): return self._client_capabilities
+    
+    @property
+    def serve_capabilities(self): return self._server_capabilities
+    
+    @property
+    def connected(self): return self._connected
+    
+    @property
+    def id(self): return self._id    
+
+    class HelloListener:
+        
+        def __init__(self, session):
+            self._session = session
+        
+        def _done(self, err=None):
+            if err is not None:
+                self._session._error = err
+            self._session.remove_listener(self)
+            self._session._init_event.set()
+        
+        ### Events
+        
+        def reply(self, data):
+            err = None
+            try:
+                id, capabilities = hello.parse(data)
+                logger.debug('session_id: %s | capabilities: \n%s', id, capabilities)
+                self._session._id, self._session.capabilities = id, capabilities
+            except Exception as e:
+                err = e
+            finally:
+                self._done(err)
+        
+        def close(self, err):
+            self._done(err)
diff --git a/ncclient/ssh.py b/ncclient/ssh.py
new file mode 100644 (file)
index 0000000..43500d8
--- /dev/null
@@ -0,0 +1,116 @@
+# 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 logging
+import paramiko
+
+from session import Session, SessionError
+
+logger = logging.getLogger('ncclient.ssh')
+
+class SessionCloseError(SessionError):
+    
+    def __str__(self):
+        return 'RECEIVED: %s | UNSENT: %s' % (self._in_buf, self._out_buf)
+    
+    def __init__(self, in_buf, out_buf):
+        SessionError.__init__(self)
+        self._in_buf, self._out_buf = in_buf, out_buf
+
+class SSHSession(Session):
+
+    BUF_SIZE = 4096
+    MSG_DELIM = ']]>]]>'
+    
+    def __init__(self, load_known_hosts=True,
+                 missing_host_key_policy=paramiko.RejectPolicy):
+        Session.__init__(self)
+        self._in_buf = ''
+        self._out_buf = ''
+        self._client = paramiko.SSHClient()
+        if load_known_hosts:
+            self._client.load_system_host_keys()
+        self._client.set_missing_host_key_policy(missing_host_key_policy)
+    
+    def load_host_keys(self, filename):
+        self._client.load_host_keys(filename)
+    
+    def set_missing_host_key_policy(self, policy):
+        self._client.set_missing_host_key_policy(policy)
+    
+    # paramiko exceptions ok?
+    # user might be looking for ClientError
+    def connect(self, hostname, port=830, username=None, password=None,
+                key_filename=None, timeout=None, allow_agent=True,
+                look_for_keys=True):
+        self._client.connect(hostname, port=port, username=username,
+                            password=password, key_filename=key_filename,
+                            timeout=timeout, allow_agent=allow_agent,
+                            look_for_keys=look_for_keys)    
+        transport = self._client.get_transport()
+        self._channel = transport.open_session()
+        self._channel.invoke_subsystem('netconf')
+        self._channel.set_name('netconf')
+        self._init()
+
+    def _close(self):
+        self._channel.close()
+        Session._close(self)
+    
+    def run(self):
+        
+        chan = self._channel
+        chan.setblocking(0)
+        q = self._q
+        
+        while True:
+            
+            if chan.closed:
+                break
+            
+            if chan.recv_ready():
+                data = chan.recv(SSHSession.BUF_SIZE)
+                if data:
+                    self._in_buf += data
+                    while True:
+                        before, delim, after = self._in_buf.partition(SSHSession.MSG_DELIM)
+                        if delim:
+                            self.dispatch('reply', before)
+                            self._in_buf = after
+                        else:
+                            break
+                else:
+                    break
+            
+            if chan.send_ready():
+                if not q.empty():
+                    msg = q.get()
+                    self._out_buf += ( msg + SSHSession.MSG_DELIM )
+                    while self._out_buf:
+                        n = chan.send(self._out_buf)
+                        if n <= 0:
+                            break
+                        self._out_buf = self._out_buf[n:]
+        
+        logger.debug('** broke out of main loop **')
+        self.dispatch('close', SessionCloseError(self._in_buf, self._out_buf))
+
+class MissingHostKeyPolicy(paramiko.MissingHostKeyPolicy):
+    
+    def __init__(self, cb):
+        self._cb = cb
+    
+    def missing_host_key(self, client, hostname, key):
+        if not self._cb(hostname, key):
+            raise SSHError
\ No newline at end of file