class OperationError(NCClientError):
pass
+class OperationError(NCClientError):
+ pass
+
class ContentError(NCClientError):
pass
"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:
### 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'
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
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
--- /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 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
# See the License for the specific language governing permissions and
# limitations under the License.
-from rpc import RPC
+from ncclient.rpc import RPC
# TODO
'''
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)
'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 = {
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 = {
# 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)
'Session-related NETCONF operations'
-from rpc import RPC
+from ncclient.rpc import RPC
from copy import deepcopy
class CloseSession(RPC):
--- /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 rpc import RPC
+from reply import RPCReply
+
+class ReplyTimeoutError(Exception):
+ pass
+
+__all__ = [
+ 'RPC',
+ 'RPCReply',
+ 'ReplyTimeoutError'
+]
--- /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 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)
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):
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')
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
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'
return self._reply
@property
- def is_async(self):
- return self._async
-
- @property
def id(self):
return self._id
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)
@property
def id(self):
return self._id
+
+ @property
+ def can_pipeline(self):
+ return True
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