# 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):
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'
+])
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
@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:
"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.
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)
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:
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):
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)
# 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):
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):
# TODO - a context manager around some <target> would be real neat
-class Lock(RPC):
+class Lock(RPC): # x
SPEC = {
'tag': 'lock',
}
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',
}
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
# 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)
'Session-related NETCONF operations'
from ncclient.rpc import RPC
-from copy import deepcopy
-class CloseSession(RPC):
+class CloseSession(RPC): # x
'CloseSession is always synchronous'
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)
--- /dev/null
+#!/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
from rpc import RPC
from reply import RPCReply
-class ReplyTimeoutError(Exception):
- pass
+from ncclient import RPCError
+
+class ReplyTimeoutError(RPCError): pass
__all__ = [
'RPC',
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
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
}
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
'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()
# limitations under the License.
from threading import Event
+from Queue import Queue
from ncclient.capabilities import Capabilities, CAPABILITIES
from ncclient.glue import 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
"Subclass implements"
raise NotImplementedError
+ def send(self, message):
+ "TODO: docstring"
+ logger.debug('queueing %s' % message)
+ self._q.put(message)
+
### Properties
@property
from ncclient.glue import Listener
-import logging
-logger = logging.getLogger('PrintListener')
-
class PrintListener(Listener):
def callback(self, root, raw):