--- /dev/null
+# 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"
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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)
--- /dev/null
+# 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