From 2acc860ab5da8380a9b79feab8d5b09ca11eaa5e Mon Sep 17 00:00:00 2001 From: Shikhar Bhushan Date: Wed, 22 Apr 2009 23:34:31 +0000 Subject: [PATCH] made content module object-oriented; lots of code organization changes overall git-svn-id: http://ncclient.googlecode.com/svn/trunk@41 6dbcf712-26ac-11de-a2f3-1373824ab735 --- ncclient/capabilities.py | 12 ++-- ncclient/content.py | 133 +++++++++++++++++++++++++++++---------- ncclient/listeners.py | 28 +++++++-- ncclient/operations/__init__.py | 34 ++++++++++ ncclient/rpc.py | 15 +++-- ncclient/session.py | 52 ++++++--------- ncclient/ssh.py | 43 +++++++------ 7 files changed, 215 insertions(+), 102 deletions(-) create mode 100644 ncclient/operations/__init__.py diff --git a/ncclient/capabilities.py b/ncclient/capabilities.py index 2165383..a446fb8 100644 --- a/ncclient/capabilities.py +++ b/ncclient/capabilities.py @@ -25,8 +25,11 @@ class Capabilities: def __contains__(self, key): return ( key in self._dict ) or ( key in self._dict.values() ) + def __iter__(self): + return self._dict.keys().__iter__() + def __repr__(self): - return self.to_xml() + return repr(self._dict.keys()) def add(self, uri, shorthand=None): if shorthand is None: @@ -44,10 +47,6 @@ class Capabilities: del self._dict[uri] break - def to_xml(self): - elems = ['%s' % uri for uri in self._dict] - return ('%s' % ''.join(elems)) - @staticmethod def guess_shorthand(uri): if uri.startswith('urn:ietf:params:netconf:capability:'): @@ -69,5 +68,4 @@ CAPABILITIES = Capabilities([ ]) if __name__ == "__main__": - assert(':validate' in CAPABILITIES) # test __contains__ - print CAPABILITIES # test __repr__ \ No newline at end of file + assert(':validate' in CAPABILITIES) # test __contains__ \ No newline at end of file diff --git a/ncclient/content.py b/ncclient/content.py index 861a885..a82426c 100644 --- a/ncclient/content.py +++ b/ncclient/content.py @@ -14,43 +14,110 @@ import logging from xml.etree import cElementTree as ElementTree +from cStringIO import StringIO logger = logging.getLogger('ncclient.content') -BASE_NS = 'urn:ietf:params:xml:ns:netconf:base:1.0' -NOTIFICATION_NS = 'urn:ietf:params:xml:ns:netconf:notification:1.0' - -def qualify(tag, ns=BASE_NS): - return '{%s}%s' % (ns, tag) +def qualify(tag, ns=None): + if ns is None: + return tag + else: + return '{%s}%s' % (ns, tag) _ = qualify -def make_hello(capabilities): - return '%s' % (BASE_NS, capabilities) - -def make_rpc(id, op): - return '%s' % (id, BASE_NS, op) - -def parse_hello(raw): - from capabilities import Capabilities - id, capabilities = 0, Capabilities() - root = ElementTree.fromstring(raw) - if root.tag == _('hello'): - for child in root.getchildren(): - if child.tag == _('session-id'): - id = int(child.text) - elif child.tag == _('capabilities'): - for cap in child.getiterator(_('capability')): - capabilities.add(cap.text) - return id, capabilities - -def parse_message_root(raw): - from cStringIO import StringIO - fp = StringIO(raw) - for event, element in ElementTree.iterparse(fp, events=('start',)): - if element.tag == _('rpc'): - return element.attrib['message-id'] - elif element.tag == _('notification', NOTIFICATION_NS): - return 'notification' + +class RootElementParser: + + '''Parse the root element of an XML document. The tag and namespace of + recognized elements, and attributes of interest can be customized. + + RootElementParser does not parse any sub-elements. + ''' + + def __init__(self, recognize=[]): + self._recognize = recognize + + def recognize(self, element): + '''Specify an element that should be successfully parsed. + + element should be a string that represents a qualified name of the form + *{namespace}tag*. + ''' + self._recognize.append((element, attrs)) + + def parse(self, raw): + '''Parse the root element from a string representing an XML document. + + Returns a (tag, attributes) tuple. tag is a string representing + the qualified name of the recognized element. attributes is a + {'attr': value} dictionary. + ''' + fp = StringIO(raw) + for event, element in ElementTree.iterparse(fp, events=('start',)): + for e in self._recognize: + if element.tag == e: + return (element.tag, element.attrib) + break + return None + + +########### + +class XMLBuilder: + + @staticmethod + def _element(node): + element = ElementTree.Element( _(node.get('tag'), + node.get('namespace', None)), + node.get('attributes', {})) + if node.has_key('children'): + for child in node['children']: + element.append(_make_element(child)) else: - return None \ No newline at end of file + return element + + @staticmethod + def _etree(tree_dict): + return ElementTree.ElementTree(XMLBuilder._element(tree_dict)) + + @staticmethod + def to_xml(tree_dict, encoding='utf-8'): + fp = StringIO() + self._etree(tree_dict).write(fp, encoding) + return fp.get_value() + + +### Hello exchange + +class Hello: + + NS = 'urn:ietf:params:xml:ns:netconf:base:1.0' + + @staticmethod + def build(capabilities, encoding='utf-8'): + hello = ElementTree.Element(_('hello', Hello.NS)) + caps = ElementTree.Element('capabilities') + for uri in capabilities: + cap = ElementTree.Element('capability') + cap.text = uri + caps.append(cap) + hello.append(caps) + tree = ElementTree.ElementTree(hello) + fp = StringIO() + tree.write(fp, encoding) + return fp.getvalue() + + @staticmethod + def parse(raw): + 'Returns tuple of (session-id, ["capability_uri", ...])' + id, capabilities = 0, [] + root = ElementTree.fromstring(raw) + if root.tag == _('hello', Hello.NS): + for child in root.getchildren(): + if child.tag == _('session-id', Hello.NS): + id = int(child.text) + elif child.tag == _('capabilities', Hello.NS): + for cap in child.getiterator(_('capability', Hello.NS)): + capabilities.append(cap.text) + return id, capabilities diff --git a/ncclient/listeners.py b/ncclient/listeners.py index 3169746..096a46e 100644 --- a/ncclient/listeners.py +++ b/ncclient/listeners.py @@ -32,13 +32,12 @@ class SessionListener(object): def __init__(self): self._id2rpc = WeakValueDictionary() self._expecting_close = False - self._subscription = None + sself._notification_rpc_id = None def __str__(self): return 'SessionListener' - def set_subscription(self, id): - self._subscription = id + def expect_close(self): self._expecting_close = True @@ -68,6 +67,27 @@ class SessionListener(object): if not self._expecting_close: raise err + +class HelloListener: + + def __str__(self): + return 'HelloListener' + + def __init__(self, session): + self._session = session + + def reply(self, data): + try: + id, capabilities = content.Hello.parse(data) + logger.debug('HelloListener: session_id: %s; capabilities: %s', id, capabilities) + self._session.initialize(id, capabilities) + except Exception as e: + self._session.initialize_error(e) + + def error(self, err): + self._session.initialize_error(err) + + class DebugListener: def __str__(self): @@ -77,4 +97,4 @@ class DebugListener: logger.debug('DebugListener:reply:\n%s' % raw) def error(self, err): - logger.debug('DebugListener:error:\n%s' % err) + logger.debug('DebugListener:error:\n%s' % err) \ No newline at end of file diff --git a/ncclient/operations/__init__.py b/ncclient/operations/__init__.py new file mode 100644 index 0000000..9229c58 --- /dev/null +++ b/ncclient/operations/__init__.py @@ -0,0 +1,34 @@ +# 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 ncclient import content as content +from ncclient.capabilities import CAPABILITIES + +from retrieve import Get, GetConfig +from edit import EditConfig, DeleteConfig +from session import CloseSession, KillSession +from lock import Lock, Unlock +from notification import CreateSubscription + +__all__ = [ + 'Get', + 'GetConfig', + 'EditConfig', + 'DeleteConfig', + 'Lock', + 'Unlock', + 'CloseSession', + 'KillSession', + 'CreateSubscription', + ] \ No newline at end of file diff --git a/ncclient/rpc.py b/ncclient/rpc.py index c186bb0..2fe5ffb 100644 --- a/ncclient/rpc.py +++ b/ncclient/rpc.py @@ -20,6 +20,11 @@ from listeners import session_listener_factory class RPC: + metadata = { + 'tag': 'rpc', + 'xmlns': 'urn:ietf:params:xml:ns:netconf:base:1.0', + } + def __init__(self, session, async=False, parse=True): self._session = session self._async = async @@ -36,6 +41,7 @@ class RPC: def _do_request(self, op): self._session.send(content.make_rpc(self._id, op)) + # content.make(RPC, attrs={'message-id': self._id}, children=(op,)) if not self._async: self._reply_event.wait() return self._reply @@ -66,9 +72,8 @@ class RPC: def session(self): return self._session - class RPCReply: - pass - -class RPCError: - pass \ No newline at end of file + + class RPCError: + + pass \ No newline at end of file diff --git a/ncclient/session.py b/ncclient/session.py index dbafb3b..9d8ae06 100644 --- a/ncclient/session.py +++ b/ncclient/session.py @@ -17,7 +17,7 @@ from threading import Thread, Event from Queue import Queue import content -from capabilities import CAPABILITIES +from capabilities import Capabilities, CAPABILITIES from error import ClientError from subject import Subject @@ -29,7 +29,7 @@ class Session(Thread, Subject): def __init__(self): Thread.__init__(self, name='session') - Subject.__init__(self, listeners=[Session.HelloListener(self)]) + Subject.__init__(self, listeners=[HelloListener(self)]) self._client_capabilities = CAPABILITIES self._server_capabilities = None # yet self._id = None # session-id @@ -42,7 +42,7 @@ class Session(Thread, Subject): # start the subclass' main loop self.start() # queue client's hello message for sending - self.send(content.make_hello(self._client_capabilities)) + self.send(content.Hello.build(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 @@ -50,9 +50,15 @@ class Session(Thread, Subject): self._close() raise self._error + def initialize(self, id, capabilities): + self._id, self._capabilities = id, Capabilities(capabilities) + self._init_event.set() + + def initialize_error(self, err): + self._error = err + self._init_event.set() + def send(self, message): - message = (u'%s' % - message).encode('utf-8') logger.debug('queueing message: \n%s' % message) self._q.put(message) @@ -61,9 +67,15 @@ class Session(Thread, Subject): def run(self): raise NotImplementedError + + def capabilities(self, whose='client'): + if whose == 'client': + return self._client_capabilities + elif whose == 'server': + return self._server_capabilities ### Properties - + @property def client_capabilities(self): return self._client_capabilities @@ -79,31 +91,3 @@ class Session(Thread, Subject): @property def id(self): return self._id - - class HelloListener: - - def __str__(self): - return '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() - - def reply(self, data): - err = None - try: - id, capabilities = content.parse_hello(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 error(self, err): - self._done(err) diff --git a/ncclient/ssh.py b/ncclient/ssh.py index 7d4be34..9f64f90 100644 --- a/ncclient/ssh.py +++ b/ncclient/ssh.py @@ -15,9 +15,11 @@ import logging from cStringIO import StringIO from os import SEEK_CUR +import socket import paramiko + from session import Session, SessionError logger = logging.getLogger('ncclient.ssh') @@ -39,7 +41,7 @@ class SSHSession(Session): MSG_DELIM = ']]>]]>' def __init__(self, load_known_hosts=True, - missing_host_key_policy=paramiko.RejectPolicy): + missing_host_key_policy=paramiko.RejectPolicy()): Session.__init__(self) self._client = paramiko.SSHClient() self._channel = None @@ -93,27 +95,30 @@ class SSHSession(Session): self._parsing_state = state self._parsing_pos = self._in_buf.tell() - 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 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) + # + #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._connected = True + # self._post_connect() + 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._connected = True - self._post_connect() + self._transport = paramiko.Transport() def run(self): chan = self._channel -- 1.7.10.4