contrib: import ncclient library (NETCONF clients)
NETCONF clients https://github.com/leopoul/ncclient/ Signed-off-by: FUJITA Tomonori <fujita.tomonori@lab.ntt.co.jp>
This commit is contained in:
parent
d1a87e87c1
commit
8649e9e153
22
ryu/contrib/ncclient/__init__.py
Normal file
22
ryu/contrib/ncclient/__init__.py
Normal file
@ -0,0 +1,22 @@
|
||||
# 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 sys
|
||||
|
||||
if sys.version_info < (2, 6):
|
||||
raise RuntimeError('You need Python 2.6+ for this module.')
|
||||
|
||||
class NCClientError(Exception):
|
||||
"Base type for all NCClient errors"
|
||||
pass
|
68
ryu/contrib/ncclient/capabilities.py
Normal file
68
ryu/contrib/ncclient/capabilities.py
Normal file
@ -0,0 +1,68 @@
|
||||
# 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.
|
||||
|
||||
def _abbreviate(uri):
|
||||
if uri.startswith("urn:ietf:params") and ":netconf:" in uri:
|
||||
splitted = uri.split(":")
|
||||
if ":capability:" in uri:
|
||||
if uri.startswith("urn:ietf:params:xml:ns:netconf"):
|
||||
name, version = splitted[7], splitted[8]
|
||||
else:
|
||||
name, version = splitted[5], splitted[6]
|
||||
return [ ":" + name, ":" + name + ":" + version ]
|
||||
elif ":base:" in uri:
|
||||
if uri.startswith("urn:ietf:params:xml:ns:netconf"):
|
||||
return [ ":base", ":base" + ":" + splitted[7] ]
|
||||
else:
|
||||
return [ ":base", ":base" + ":" + splitted[5] ]
|
||||
return []
|
||||
|
||||
def schemes(url_uri):
|
||||
"Given a URI that has a *scheme* query string (i.e. `:url` capability URI), will return a list of supported schemes."
|
||||
return url_uri.partition("?scheme=")[2].split(",")
|
||||
|
||||
class Capabilities:
|
||||
|
||||
"Represents the set of capabilities available to a NETCONF client or server. It is initialized with a list of capability URI's."
|
||||
|
||||
def __init__(self, capabilities):
|
||||
self._dict = {}
|
||||
for uri in capabilities:
|
||||
self._dict[uri] = _abbreviate(uri)
|
||||
|
||||
def __contains__(self, key):
|
||||
if key in self._dict:
|
||||
return True
|
||||
for abbrs in self._dict.values():
|
||||
if key in abbrs:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __len__(self):
|
||||
return len(self._dict)
|
||||
|
||||
def __iter__(self):
|
||||
return self._dict.iterkeys()
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self._dict.keys())
|
||||
|
||||
def add(self, uri):
|
||||
"Add a capability."
|
||||
self._dict[uri] = _abbreviate(uri)
|
||||
|
||||
def remove(self, uri):
|
||||
"Remove a capability."
|
||||
if key in self._dict:
|
||||
del self._dict[key]
|
24
ryu/contrib/ncclient/debug.py
Normal file
24
ryu/contrib/ncclient/debug.py
Normal file
@ -0,0 +1,24 @@
|
||||
# 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.transport import SessionListener
|
||||
|
||||
class PrintListener(SessionListener):
|
||||
|
||||
def callback(self, root, raw):
|
||||
print('\n# RECEIVED MESSAGE with root=[tag=%r, attrs=%r] #\n%r\n' %
|
||||
(root[0], root[1], raw))
|
||||
|
||||
def errback(self, err):
|
||||
print('\n# RECEIVED ERROR #\n%r\n' % err)
|
177
ryu/contrib/ncclient/manager.py
Normal file
177
ryu/contrib/ncclient/manager.py
Normal file
@ -0,0 +1,177 @@
|
||||
# Copyright 2009 Shikhar Bhushan
|
||||
# Copyright 2011 Leonidas Poulopoulos
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""This module is a thin layer of abstraction around the library. It exposes all core functionality."""
|
||||
|
||||
import capabilities
|
||||
import operations
|
||||
import transport
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('ncclient.manager')
|
||||
|
||||
CAPABILITIES = [
|
||||
"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?scheme=http,ftp,file,https,sftp",
|
||||
"urn:ietf:params:netconf:capability:validate:1.0",
|
||||
"urn:ietf:params:netconf:capability:xpath:1.0",
|
||||
"urn:liberouter:params:netconf:capability:power-control:1.0"
|
||||
"urn:ietf:params:netconf:capability:interleave:1.0"
|
||||
]
|
||||
"""A list of URI's representing the client's capabilities. This is used during the initial capability exchange. Modify this if you need to announce some capability not already included."""
|
||||
|
||||
OPERATIONS = {
|
||||
"get": operations.Get,
|
||||
"get_config": operations.GetConfig,
|
||||
"dispatch": operations.Dispatch,
|
||||
"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,
|
||||
"poweroff_machine": operations.PoweroffMachine,
|
||||
"reboot_machine": operations.RebootMachine
|
||||
}
|
||||
"""Dictionary of method names and corresponding :class:`~ncclient.operations.RPC` subclasses. It is used to lookup operations, e.g. `get_config` is mapped to :class:`~ncclient.operations.GetConfig`. It is thus possible to add additional operations to the :class:`Manager` API."""
|
||||
|
||||
def connect_ssh(*args, **kwds):
|
||||
"""Initialize a :class:`Manager` over the SSH transport. For documentation of arguments see :meth:`ncclient.transport.SSHSession.connect`.
|
||||
|
||||
The underlying :class:`ncclient.transport.SSHSession` is created with :data:`CAPABILITIES`. It is first instructed to :meth:`~ncclient.transport.SSHSession.load_known_hosts` and then all the provided arguments are passed directly to its implementation of :meth:`~ncclient.transport.SSHSession.connect`.
|
||||
"""
|
||||
session = transport.SSHSession(capabilities.Capabilities(CAPABILITIES))
|
||||
session.load_known_hosts()
|
||||
session.connect(*args, **kwds)
|
||||
return Manager(session)
|
||||
|
||||
connect = connect_ssh
|
||||
"Same as :func:`connect_ssh`, since SSH is the default (and currently, the only) transport."
|
||||
|
||||
class OpExecutor(type):
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
def make_wrapper(op_cls):
|
||||
def wrapper(self, *args, **kwds):
|
||||
return self.execute(op_cls, *args, **kwds)
|
||||
wrapper.func_doc = op_cls.request.func_doc
|
||||
return wrapper
|
||||
for op_name, op_cls in OPERATIONS.iteritems():
|
||||
attrs[op_name] = make_wrapper(op_cls)
|
||||
return super(OpExecutor, cls).__new__(cls, name, bases, attrs)
|
||||
|
||||
class Manager(object):
|
||||
|
||||
"""For details on the expected behavior of the operations and their parameters refer to :rfc:`4741`.
|
||||
|
||||
Manager instances are also context managers so you can use it like this::
|
||||
|
||||
with manager.connect("host") as m:
|
||||
# do your stuff
|
||||
|
||||
... or like this::
|
||||
|
||||
m = manager.connect("host")
|
||||
try:
|
||||
# do your stuff
|
||||
finally:
|
||||
m.close_session()
|
||||
"""
|
||||
|
||||
__metaclass__ = OpExecutor
|
||||
|
||||
def __init__(self, session, timeout=30):
|
||||
self._session = session
|
||||
self._async_mode = False
|
||||
self._timeout = timeout
|
||||
self._raise_mode = operations.RaiseMode.ALL
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close_session()
|
||||
return False
|
||||
|
||||
def __set_timeout(self, timeout):
|
||||
self._timeout = timeout
|
||||
|
||||
def __set_async_mode(self, mode):
|
||||
self._async_mode = mode
|
||||
|
||||
def __set_raise_mode(self, mode):
|
||||
assert(mode in (operations.RaiseMode.NONE, operations.RaiseMode.ERRORS, operations.RaiseMode.ALL))
|
||||
self._raise_mode = mode
|
||||
|
||||
def execute(self, cls, *args, **kwds):
|
||||
return cls(self._session,
|
||||
async=self._async_mode,
|
||||
timeout=self._timeout,
|
||||
raise_mode=self._raise_mode).request(*args, **kwds)
|
||||
|
||||
def locked(self, target):
|
||||
"""Returns a context manager for a lock on a datastore, where *target* is the name of the configuration datastore to lock, e.g.::
|
||||
|
||||
with m.locked("running"):
|
||||
# do your stuff
|
||||
|
||||
... instead of::
|
||||
|
||||
m.lock("running")
|
||||
try:
|
||||
# do your stuff
|
||||
finally:
|
||||
m.unlock("running")
|
||||
"""
|
||||
return operations.LockContext(self._session, target)
|
||||
|
||||
@property
|
||||
def client_capabilities(self):
|
||||
":class:`~ncclient.capabilities.Capabilities` object representing the client's capabilities."
|
||||
return self._session._client_capabilities
|
||||
|
||||
@property
|
||||
def server_capabilities(self):
|
||||
":class:`~ncclient.capabilities.Capabilities` object representing the server's capabilities."
|
||||
return self._session._server_capabilities
|
||||
|
||||
@property
|
||||
def session_id(self):
|
||||
"`session-id` assigned by the NETCONF server."
|
||||
return self._session.id
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"Whether currently connected to the NETCONF server."
|
||||
return self._session.connected
|
||||
|
||||
async_mode = property(fget=lambda self: self._async_mode, fset=__set_async_mode)
|
||||
"Specify whether operations are executed asynchronously (`True`) or synchronously (`False`) (the default)."
|
||||
|
||||
timeout = property(fget=lambda self: self._timeout, fset=__set_timeout)
|
||||
"Specify the timeout for synchronous RPC requests."
|
||||
|
||||
raise_mode = property(fget=lambda self: self._raise_mode, fset=__set_raise_mode)
|
||||
"Specify which errors are raised as :exc:`~ncclient.operations.RPCError` exceptions. Valid values are the constants defined in :class:`~ncclient.operations.RaiseMode`. The default value is :attr:`~ncclient.operations.RaiseMode.ALL`."
|
51
ryu/contrib/ncclient/operations/__init__.py
Normal file
51
ryu/contrib/ncclient/operations/__init__.py
Normal file
@ -0,0 +1,51 @@
|
||||
# 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 errors import OperationError, TimeoutExpiredError, MissingCapabilityError
|
||||
from rpc import RPC, RPCReply, RPCError, RaiseMode
|
||||
|
||||
# rfc4741 ops
|
||||
from retrieve import Get, GetConfig, GetReply, Dispatch
|
||||
from edit import EditConfig, CopyConfig, DeleteConfig, Validate, Commit, DiscardChanges
|
||||
from session import CloseSession, KillSession
|
||||
from lock import Lock, Unlock, LockContext
|
||||
# others...
|
||||
from flowmon import PoweroffMachine, RebootMachine
|
||||
|
||||
__all__ = [
|
||||
'RPC',
|
||||
'RPCReply',
|
||||
'RPCError',
|
||||
'RaiseMode',
|
||||
'Get',
|
||||
'GetConfig',
|
||||
'Dispatch',
|
||||
'GetReply',
|
||||
'EditConfig',
|
||||
'CopyConfig',
|
||||
'Validate',
|
||||
'Commit',
|
||||
'DiscardChanges',
|
||||
'DeleteConfig',
|
||||
'Lock',
|
||||
'Unlock',
|
||||
'PoweroffMachine',
|
||||
'RebootMachine',
|
||||
'LockContext',
|
||||
'CloseSession',
|
||||
'KillSession',
|
||||
'OperationError',
|
||||
'TimeoutExpiredError',
|
||||
'MissingCapabilityError'
|
||||
]
|
143
ryu/contrib/ncclient/operations/edit.py
Normal file
143
ryu/contrib/ncclient/operations/edit.py
Normal file
@ -0,0 +1,143 @@
|
||||
# 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.xml_ import *
|
||||
|
||||
from rpc import RPC
|
||||
|
||||
import util
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("ncclient.operations.edit")
|
||||
|
||||
"Operations related to changing device configuration"
|
||||
|
||||
class EditConfig(RPC):
|
||||
"`edit-config` RPC"
|
||||
|
||||
def request(self, target, config, default_operation=None, test_option=None, error_option=None):
|
||||
"""Loads all or part of the specified *config* to the *target* configuration datastore.
|
||||
|
||||
*target* is the name of the configuration datastore being edited
|
||||
|
||||
*config* is the configuration, which must be rooted in the `config` element. It can be specified either as a string or an :class:`~xml.etree.ElementTree.Element`.
|
||||
|
||||
*default_operation* if specified must be one of { `"merge"`, `"replace"`, or `"none"` }
|
||||
|
||||
*test_option* if specified must be one of { `"test_then_set"`, `"set"` }
|
||||
|
||||
*error_option* if specified must be one of { `"stop-on-error"`, `"continue-on-error"`, `"rollback-on-error"` }
|
||||
|
||||
The `"rollback-on-error"` *error_option* depends on the `:rollback-on-error` capability.
|
||||
"""
|
||||
node = new_ele("edit-config")
|
||||
node.append(util.datastore_or_url("target", target, self._assert))
|
||||
if error_option is not None:
|
||||
if error_option == "rollback-on-error":
|
||||
self._assert(":rollback-on-error")
|
||||
sub_ele(node, "error-option").text = error_option
|
||||
if test_option is not None:
|
||||
self._assert(':validate')
|
||||
sub_ele(node, "test-option").text = test_option
|
||||
if default_operation is not None:
|
||||
# TODO: check if it is a valid default-operation
|
||||
sub_ele(node, "default-operation").text = default_operation
|
||||
node.append(validated_element(config, ("config", qualify("config"))))
|
||||
return self._request(node)
|
||||
|
||||
|
||||
class DeleteConfig(RPC):
|
||||
"`delete-config` RPC"
|
||||
|
||||
def request(self, target):
|
||||
"""Delete a configuration datastore.
|
||||
|
||||
*target* specifies the name or URL of configuration datastore to delete
|
||||
|
||||
:seealso: :ref:`srctarget_params`"""
|
||||
node = new_ele("delete-config")
|
||||
node.append(util.datastore_or_url("target", target, self._assert))
|
||||
return self._request(node)
|
||||
|
||||
|
||||
class CopyConfig(RPC):
|
||||
"`copy-config` RPC"
|
||||
|
||||
def request(self, source, target):
|
||||
"""Create or replace an entire configuration datastore with the contents of another complete
|
||||
configuration datastore.
|
||||
|
||||
*source* is the name of the configuration datastore to use as the source of the copy operation or `config` element containing the configuration subtree to copy
|
||||
|
||||
*target* is the name of the configuration datastore to use as the destination of the copy operation
|
||||
|
||||
:seealso: :ref:`srctarget_params`"""
|
||||
node = new_ele("copy-config")
|
||||
node.append(util.datastore_or_url("target", target, self._assert))
|
||||
node.append(util.datastore_or_url("source", source, self._assert))
|
||||
return self._request(node)
|
||||
|
||||
|
||||
class Validate(RPC):
|
||||
"`validate` RPC. Depends on the `:validate` capability."
|
||||
|
||||
DEPENDS = [':validate']
|
||||
|
||||
def request(self, source):
|
||||
"""Validate the contents of the specified configuration.
|
||||
|
||||
*source* is the name of the configuration datastore being validated or `config` element containing the configuration subtree to be validated
|
||||
|
||||
:seealso: :ref:`srctarget_params`"""
|
||||
node = new_ele("validate")
|
||||
try:
|
||||
src = validated_element(source, ("config", qualify("config")))
|
||||
except Exception as e:
|
||||
logger.debug(e)
|
||||
src = util.datastore_or_url("source", source, self._assert)
|
||||
(node if src.tag == "source" else sub_ele(node, "source")).append(src)
|
||||
return self._request(node)
|
||||
|
||||
|
||||
class Commit(RPC):
|
||||
"`commit` RPC. Depends on the `:candidate` capability, and the `:confirmed-commit`."
|
||||
|
||||
DEPENDS = [':candidate']
|
||||
|
||||
def request(self, confirmed=False, timeout=None):
|
||||
"""Commit the candidate configuration as the device's new current configuration. Depends on the `:candidate` capability.
|
||||
|
||||
A confirmed commit (i.e. if *confirmed* is `True`) is reverted if there is no followup commit within the *timeout* interval. If no timeout is specified the confirm timeout defaults to 600 seconds (10 minutes). A confirming commit may have the *confirmed* parameter but this is not required. Depends on the `:confirmed-commit` capability.
|
||||
|
||||
*confirmed* whether this is a confirmed commit
|
||||
|
||||
*timeout* specifies the confirm timeout in seconds"""
|
||||
node = new_ele("commit")
|
||||
if confirmed:
|
||||
self._assert(":confirmed-commit")
|
||||
sub_ele(node, "confirmed")
|
||||
if timeout is not None:
|
||||
sub_ele(node, "confirm-timeout").text = timeout
|
||||
return self._request(node)
|
||||
|
||||
|
||||
class DiscardChanges(RPC):
|
||||
"`discard-changes` RPC. Depends on the `:candidate` capability."
|
||||
|
||||
DEPENDS = [":candidate"]
|
||||
|
||||
def request(self):
|
||||
"""Revert the candidate configuration to the currently running configuration. Any uncommitted changes are discarded."""
|
||||
return self._request(new_ele("discard-changes"))
|
24
ryu/contrib/ncclient/operations/errors.py
Normal file
24
ryu/contrib/ncclient/operations/errors.py
Normal file
@ -0,0 +1,24 @@
|
||||
# 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 NCClientError
|
||||
|
||||
class OperationError(NCClientError):
|
||||
pass
|
||||
|
||||
class TimeoutExpiredError(NCClientError):
|
||||
pass
|
||||
|
||||
class MissingCapabilityError(NCClientError):
|
||||
pass
|
39
ryu/contrib/ncclient/operations/flowmon.py
Normal file
39
ryu/contrib/ncclient/operations/flowmon.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright 2h009 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.
|
||||
|
||||
'Power-control operations'
|
||||
|
||||
from ncclient.xml_ import *
|
||||
|
||||
from rpc import RPC
|
||||
|
||||
PC_URN = "urn:liberouter:params:xml:ns:netconf:power-control:1.0"
|
||||
|
||||
class PoweroffMachine(RPC):
|
||||
|
||||
"*poweroff-machine* RPC (flowmon)"
|
||||
|
||||
DEPENDS = ["urn:liberouter:param:netconf:capability:power-control:1.0"]
|
||||
|
||||
def request(self):
|
||||
return self._request(new_ele(qualify("poweroff-machine", PC_URN)))
|
||||
|
||||
class RebootMachine(RPC):
|
||||
|
||||
"*reboot-machine* RPC (flowmon)"
|
||||
|
||||
DEPENDS = ["urn:liberouter:params:netconf:capability:power-control:1.0"]
|
||||
|
||||
def request(self):
|
||||
return self._request(new_ele(qualify("reboot-machine", PC_URN)))
|
70
ryu/contrib/ncclient/operations/lock.py
Normal file
70
ryu/contrib/ncclient/operations/lock.py
Normal file
@ -0,0 +1,70 @@
|
||||
# Copyright 2h009 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.
|
||||
|
||||
"Locking-related NETCONF operations"
|
||||
|
||||
from ncclient.xml_ import *
|
||||
|
||||
from rpc import RaiseMode, RPC
|
||||
|
||||
# TODO: parse session-id from a lock-denied error, and raise a tailored exception?
|
||||
|
||||
class Lock(RPC):
|
||||
|
||||
"`lock` RPC"
|
||||
|
||||
def request(self, target):
|
||||
"""Allows the client to lock the configuration system of a device.
|
||||
|
||||
*target* is the name of the configuration datastore to lock
|
||||
"""
|
||||
node = new_ele("lock")
|
||||
sub_ele(sub_ele(node, "target"), target)
|
||||
return self._request(node)
|
||||
|
||||
|
||||
class Unlock(RPC):
|
||||
|
||||
"`unlock` RPC"
|
||||
|
||||
def request(self, target):
|
||||
"""Release a configuration lock, previously obtained with the lock operation.
|
||||
|
||||
*target* is the name of the configuration datastore to unlock
|
||||
"""
|
||||
node = new_ele("unlock")
|
||||
sub_ele(sub_ele(node, "target"), target)
|
||||
return self._request(node)
|
||||
|
||||
|
||||
class LockContext:
|
||||
|
||||
"""A context manager for the :class:`Lock` / :class:`Unlock` pair of RPC's.
|
||||
|
||||
Any `rpc-error` will be raised as an exception.
|
||||
|
||||
Initialise with (:class:`Session <ncclient.transport.Session>`) instance and lock target.
|
||||
"""
|
||||
|
||||
def __init__(self, session, target):
|
||||
self.session = session
|
||||
self.target = target
|
||||
|
||||
def __enter__(self):
|
||||
Lock(self.session, raise_mode=RaiseMode.ERRORS).request(self.target)
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
Unlock(self.session, raise_mode=RaiseMode.ERRORS).request(self.target)
|
||||
return False
|
127
ryu/contrib/ncclient/operations/retrieve.py
Normal file
127
ryu/contrib/ncclient/operations/retrieve.py
Normal file
@ -0,0 +1,127 @@
|
||||
# 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, RPCReply
|
||||
|
||||
from ncclient.xml_ import *
|
||||
|
||||
import util
|
||||
|
||||
class GetReply(RPCReply):
|
||||
|
||||
"""Adds attributes for the *data* element to `RPCReply`."""
|
||||
|
||||
def _parsing_hook(self, root):
|
||||
self._data = None
|
||||
if not self._errors:
|
||||
self._data = root.find(qualify("data"))
|
||||
|
||||
@property
|
||||
def data_ele(self):
|
||||
"*data* element as an :class:`~xml.etree.ElementTree.Element`"
|
||||
if not self._parsed:
|
||||
self.parse()
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def data_xml(self):
|
||||
"*data* element as an XML string"
|
||||
if not self._parsed:
|
||||
self.parse()
|
||||
return to_xml(self._data)
|
||||
|
||||
data = data_ele
|
||||
"Same as :attr:`data_ele`"
|
||||
|
||||
|
||||
class Get(RPC):
|
||||
|
||||
"The *get* RPC."
|
||||
|
||||
REPLY_CLS = GetReply
|
||||
"See :class:`GetReply`."
|
||||
|
||||
def request(self, filter=None):
|
||||
"""Retrieve running configuration and device state information.
|
||||
|
||||
*filter* specifies the portion of the configuration to retrieve (by default entire configuration is retrieved)
|
||||
|
||||
:seealso: :ref:`filter_params`
|
||||
"""
|
||||
node = new_ele("get")
|
||||
if filter is not None:
|
||||
node.append(util.build_filter(filter))
|
||||
return self._request(node)
|
||||
|
||||
|
||||
class GetConfig(RPC):
|
||||
|
||||
"The *get-config* RPC."
|
||||
|
||||
REPLY_CLS = GetReply
|
||||
"See :class:`GetReply`."
|
||||
|
||||
def request(self, source, filter=None):
|
||||
"""Retrieve all or part of a specified configuration.
|
||||
|
||||
*source* name of the configuration datastore being queried
|
||||
|
||||
*filter* specifies the portion of the configuration to retrieve (by default entire configuration is retrieved)
|
||||
|
||||
:seealso: :ref:`filter_params`"""
|
||||
node = new_ele("get-config")
|
||||
node.append(util.datastore_or_url("source", source, self._assert))
|
||||
if filter is not None:
|
||||
node.append(util.build_filter(filter))
|
||||
return self._request(node)
|
||||
|
||||
class Dispatch(RPC):
|
||||
|
||||
"Generic retrieving wrapper"
|
||||
|
||||
REPLY_CLS = GetReply
|
||||
"See :class:`GetReply`."
|
||||
|
||||
def request(self, rpc_command, source=None, filter=None):
|
||||
"""
|
||||
*rpc_command* specifies rpc command to be dispatched either in plain text or in xml element format (depending on command)
|
||||
|
||||
*source* name of the configuration datastore being queried
|
||||
|
||||
*filter* specifies the portion of the configuration to retrieve (by default entire configuration is retrieved)
|
||||
|
||||
:seealso: :ref:`filter_params`
|
||||
|
||||
Examples of usage::
|
||||
|
||||
dispatch('clear-arp-table')
|
||||
|
||||
or dispatch element like ::
|
||||
|
||||
xsd_fetch = new_ele('get-xnm-information')
|
||||
sub_ele(xsd_fetch, 'type').text="xml-schema"
|
||||
sub_ele(xsd_fetch, 'namespace').text="junos-configuration"
|
||||
dispatch(xsd_fetch)
|
||||
"""
|
||||
|
||||
if ET.iselement(rpc_command):
|
||||
node = rpc_command
|
||||
else:
|
||||
node = new_ele(rpc_command)
|
||||
if source is not None:
|
||||
node.append(util.datastore_or_url("source", source, self._assert))
|
||||
if filter is not None:
|
||||
node.append(util.build_filter(filter))
|
||||
return self._request(node)
|
||||
|
373
ryu/contrib/ncclient/operations/rpc.py
Normal file
373
ryu/contrib/ncclient/operations/rpc.py
Normal file
@ -0,0 +1,373 @@
|
||||
# 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, Lock
|
||||
from uuid import uuid1
|
||||
|
||||
from ncclient.xml_ import *
|
||||
from ncclient.transport import SessionListener
|
||||
|
||||
from errors import OperationError, TimeoutExpiredError, MissingCapabilityError
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger("ncclient.operations.rpc")
|
||||
|
||||
|
||||
class RPCError(OperationError):
|
||||
|
||||
"Represents an `rpc-error`. It is a type of :exc:`OperationError` and can be raised as such."
|
||||
|
||||
tag_to_attr = {
|
||||
qualify("error-type"): "_type",
|
||||
qualify("error-tag"): "_tag",
|
||||
qualify("error-severity"): "_severity",
|
||||
qualify("error-info"): "_info",
|
||||
qualify("error-path"): "_path",
|
||||
qualify("error-message"): "_message"
|
||||
}
|
||||
|
||||
def __init__(self, raw):
|
||||
self._raw = raw
|
||||
for attr in RPCError.tag_to_attr.values():
|
||||
setattr(self, attr, None)
|
||||
for subele in raw:
|
||||
attr = RPCError.tag_to_attr.get(subele.tag, None)
|
||||
if attr is not None:
|
||||
setattr(self, attr, subele.text if attr != "_info" else to_xml(subele) )
|
||||
if self.message is not None:
|
||||
OperationError.__init__(self, self.message)
|
||||
else:
|
||||
OperationError.__init__(self, self.to_dict())
|
||||
|
||||
def to_dict(self):
|
||||
return dict([ (attr[1:], getattr(self, attr)) for attr in RPCError.tag_to_attr.values() ])
|
||||
|
||||
@property
|
||||
def xml(self):
|
||||
"The `rpc-error` element as returned in XML."
|
||||
return self._raw
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"The contents of the `error-type` element."
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def tag(self):
|
||||
"The contents of the `error-tag` element."
|
||||
return self._tag
|
||||
|
||||
@property
|
||||
def severity(self):
|
||||
"The contents of the `error-severity` element."
|
||||
return self._severity
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"The contents of the `error-path` element if present or `None`."
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
"The contents of the `error-message` element if present or `None`."
|
||||
return self._message
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
"XML string or `None`; representing the `error-info` element."
|
||||
return self._info
|
||||
|
||||
|
||||
class RPCReply:
|
||||
|
||||
"""Represents an *rpc-reply*. Only concerns itself with whether the operation was successful.
|
||||
|
||||
.. note::
|
||||
If the reply has not yet been parsed there is an implicit, one-time parsing overhead to
|
||||
accessing some of the attributes defined by this class.
|
||||
"""
|
||||
|
||||
ERROR_CLS = RPCError
|
||||
"Subclasses can specify a different error class, but it should be a subclass of `RPCError`."
|
||||
|
||||
def __init__(self, raw):
|
||||
self._raw = raw
|
||||
self._parsed = False
|
||||
self._root = None
|
||||
self._errors = []
|
||||
|
||||
def __repr__(self):
|
||||
return self._raw
|
||||
|
||||
def parse(self):
|
||||
"Parses the *rpc-reply*."
|
||||
if self._parsed: return
|
||||
root = self._root = to_ele(self._raw) # The <rpc-reply> element
|
||||
# Per RFC 4741 an <ok/> tag is sent when there are no errors or warnings
|
||||
ok = root.find(qualify("ok"))
|
||||
if ok is None:
|
||||
# Create RPCError objects from <rpc-error> elements
|
||||
error = root.find(qualify("rpc-error"))
|
||||
if error is not None:
|
||||
for err in root.getiterator(error.tag):
|
||||
# Process a particular <rpc-error>
|
||||
self._errors.append(self.ERROR_CLS(err))
|
||||
self._parsing_hook(root)
|
||||
self._parsed = True
|
||||
|
||||
def _parsing_hook(self, root):
|
||||
"No-op by default. Gets passed the *root* element for the reply."
|
||||
pass
|
||||
|
||||
@property
|
||||
def xml(self):
|
||||
"*rpc-reply* element as returned."
|
||||
return self._raw
|
||||
|
||||
@property
|
||||
def ok(self):
|
||||
"Boolean value indicating if there were no errors."
|
||||
return not self.errors # empty list => false
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
"Returns the first :class:`RPCError` and `None` if there were no errors."
|
||||
self.parse()
|
||||
if self._errors:
|
||||
return self._errors[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def errors(self):
|
||||
"List of `RPCError` objects. Will be empty if there were no *rpc-error* elements in reply."
|
||||
self.parse()
|
||||
return self._errors
|
||||
|
||||
|
||||
class RPCReplyListener(SessionListener): # internal use
|
||||
|
||||
creation_lock = Lock()
|
||||
|
||||
# one instance per session -- maybe there is a better way??
|
||||
def __new__(cls, session):
|
||||
with RPCReplyListener.creation_lock:
|
||||
instance = session.get_listener_instance(cls)
|
||||
if instance is None:
|
||||
instance = object.__new__(cls)
|
||||
instance._lock = Lock()
|
||||
instance._id2rpc = {}
|
||||
#instance._pipelined = session.can_pipeline
|
||||
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 != qualify("rpc-reply"):
|
||||
return
|
||||
for key in attrs: # in the <rpc-reply> attributes
|
||||
if key == "message-id": # if we found msgid attr
|
||||
id = attrs[key] # get the msgid
|
||||
with self._lock:
|
||||
try:
|
||||
rpc = self._id2rpc[id] # the corresponding rpc
|
||||
logger.debug("Delivering to %r" % rpc)
|
||||
rpc.deliver_reply(raw)
|
||||
except KeyError:
|
||||
raise OperationError("Unknown 'message-id': %s", id)
|
||||
# no catching other exceptions, fail loudly if must
|
||||
else:
|
||||
# if no error delivering, can del the reference to the RPC
|
||||
del self._id2rpc[id]
|
||||
break
|
||||
else:
|
||||
raise OperationError("Could not find 'message-id' attribute in <rpc-reply>")
|
||||
|
||||
def errback(self, err):
|
||||
try:
|
||||
for rpc in self._id2rpc.values():
|
||||
rpc.deliver_error(err)
|
||||
finally:
|
||||
self._id2rpc.clear()
|
||||
|
||||
|
||||
class RaiseMode(object):
|
||||
|
||||
NONE = 0
|
||||
"Don't attempt to raise any type of `rpc-error` as :exc:`RPCError`."
|
||||
|
||||
ERRORS = 1
|
||||
"Raise only when the `error-type` indicates it is an honest-to-god error."
|
||||
|
||||
ALL = 2
|
||||
"Don't look at the `error-type`, always raise."
|
||||
|
||||
|
||||
class RPC(object):
|
||||
|
||||
"""Base class for all operations, directly corresponding to *rpc* requests. Handles making the request, and taking delivery of the reply."""
|
||||
|
||||
DEPENDS = []
|
||||
"""Subclasses can specify their dependencies on capabilities as a list of URI's or abbreviated names, e.g. ':writable-running'. These are verified at the time of instantiation. If the capability is not available, :exc:`MissingCapabilityError` is raised."""
|
||||
|
||||
REPLY_CLS = RPCReply
|
||||
"By default :class:`RPCReply`. Subclasses can specify a :class:`RPCReply` subclass."
|
||||
|
||||
def __init__(self, session, async=False, timeout=30, raise_mode=RaiseMode.NONE):
|
||||
"""
|
||||
*session* is the :class:`~ncclient.transport.Session` instance
|
||||
|
||||
*async* specifies whether the request is to be made asynchronously, see :attr:`is_async`
|
||||
|
||||
*timeout* is the timeout for a synchronous request, see :attr:`timeout`
|
||||
|
||||
*raise_mode* specifies the exception raising mode, see :attr:`raise_mode`
|
||||
"""
|
||||
self._session = session
|
||||
try:
|
||||
for cap in self.DEPENDS:
|
||||
self._assert(cap)
|
||||
except AttributeError:
|
||||
pass
|
||||
self._async = async
|
||||
self._timeout = timeout
|
||||
self._raise_mode = raise_mode
|
||||
self._id = uuid1().urn # Keeps things simple instead of having a class attr with running ID that has to be locked
|
||||
self._listener = RPCReplyListener(session)
|
||||
self._listener.register(self._id, self)
|
||||
self._reply = None
|
||||
self._error = None
|
||||
self._event = Event()
|
||||
|
||||
def _wrap(self, subele):
|
||||
# internal use
|
||||
ele = new_ele("rpc", {"message-id": self._id})
|
||||
ele.append(subele)
|
||||
return to_xml(ele)
|
||||
|
||||
def _request(self, op):
|
||||
"""Implementations of :meth:`request` call this method to send the request and process the reply.
|
||||
|
||||
In synchronous mode, blocks until the reply is received and returns :class:`RPCReply`. Depending on the :attr:`raise_mode` a `rpc-error` element in the reply may lead to an :exc:`RPCError` exception.
|
||||
|
||||
In asynchronous mode, returns immediately, returning `self`. The :attr:`event` attribute will be set when the reply has been received (see :attr:`reply`) or an error occured (see :attr:`error`).
|
||||
|
||||
*op* is the operation to be requested as an :class:`~xml.etree.ElementTree.Element`
|
||||
"""
|
||||
logger.info('Requesting %r' % self.__class__.__name__)
|
||||
req = self._wrap(op)
|
||||
self._session.send(req)
|
||||
if self._async:
|
||||
logger.debug('Async request, returning %r', self)
|
||||
return self
|
||||
else:
|
||||
logger.debug('Sync request, will wait for timeout=%r' % self._timeout)
|
||||
self._event.wait(self._timeout)
|
||||
if self._event.isSet():
|
||||
if self._error:
|
||||
# Error that prevented reply delivery
|
||||
raise self._error
|
||||
self._reply.parse()
|
||||
if self._reply.error is not None:
|
||||
# <rpc-error>'s [ RPCError ]
|
||||
if self._raise_mode == RaiseMode.ALL:
|
||||
raise self._reply.error
|
||||
elif (self._raise_mode == RaiseMode.ERRORS and self._reply.error.type == "error"):
|
||||
raise self._reply.error
|
||||
return self._reply
|
||||
else:
|
||||
raise TimeoutExpiredError
|
||||
|
||||
def request(self):
|
||||
"""Subclasses must implement this method. Typically only the request needs to be built as an
|
||||
:class:`~xml.etree.ElementTree.Element` and everything else can be handed off to
|
||||
:meth:`_request`."""
|
||||
pass
|
||||
|
||||
def _assert(self, capability):
|
||||
"""Subclasses can use this method to verify that a capability is available with the NETCONF
|
||||
server, before making a request that requires it. A :exc:`MissingCapabilityError` will be
|
||||
raised if the capability is not available."""
|
||||
if capability not in self._session.server_capabilities:
|
||||
raise MissingCapabilityError('Server does not support [%s]' % capability)
|
||||
|
||||
def deliver_reply(self, raw):
|
||||
# internal use
|
||||
self._reply = self.REPLY_CLS(raw)
|
||||
self._event.set()
|
||||
|
||||
def deliver_error(self, err):
|
||||
# internal use
|
||||
self._error = err
|
||||
self._event.set()
|
||||
|
||||
@property
|
||||
def reply(self):
|
||||
":class:`RPCReply` element if reply has been received or `None`"
|
||||
return self._reply
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
""":exc:`Exception` type if an error occured or `None`.
|
||||
|
||||
.. note::
|
||||
This represents an error which prevented a reply from being received. An *rpc-error*
|
||||
does not fall in that category -- see `RPCReply` for that.
|
||||
"""
|
||||
return self._error
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"The *message-id* for this RPC."
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
"The `~ncclient.transport.Session` object associated with this RPC."
|
||||
return self._session
|
||||
|
||||
@property
|
||||
def event(self):
|
||||
""":class:`~threading.Event` that is set when reply has been received or when an error preventing
|
||||
delivery of the reply occurs.
|
||||
"""
|
||||
return self._event
|
||||
|
||||
def __set_async(self, async=True):
|
||||
self._async = async
|
||||
if async and not session.can_pipeline:
|
||||
raise UserWarning('Asynchronous mode not supported for this device/session')
|
||||
|
||||
def __set_raise_mode(self, mode):
|
||||
assert(choice in ("all", "errors", "none"))
|
||||
self._raise_mode = mode
|
||||
|
||||
def __set_timeout(self, timeout):
|
||||
self._timeout = timeout
|
||||
|
||||
raise_mode = property(fget=lambda self: self._raise_mode, fset=__set_raise_mode)
|
||||
"""Depending on this exception raising mode, an `rpc-error` in the reply may be raised as an :exc:`RPCError` exception. Valid values are the constants defined in :class:`RaiseMode`. """
|
||||
|
||||
is_async = property(fget=lambda self: self._async, fset=__set_async)
|
||||
"""Specifies whether this RPC will be / was requested asynchronously. By default RPC's are synchronous."""
|
||||
|
||||
timeout = property(fget=lambda self: self._timeout, fset=__set_timeout)
|
||||
"""Timeout in seconds for synchronous waiting defining how long the RPC request will block on a reply before raising :exc:`TimeoutExpiredError`.
|
||||
|
||||
Irrelevant for asynchronous usage.
|
||||
"""
|
44
ryu/contrib/ncclient/operations/session.py
Normal file
44
ryu/contrib/ncclient/operations/session.py
Normal file
@ -0,0 +1,44 @@
|
||||
# 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.
|
||||
|
||||
"Session-related NETCONF operations"
|
||||
|
||||
from ncclient.xml_ import *
|
||||
|
||||
from rpc import RPC
|
||||
|
||||
class CloseSession(RPC):
|
||||
|
||||
"`close-session` RPC. The connection to NETCONF server is also closed."
|
||||
|
||||
def request(self):
|
||||
"Request graceful termination of the NETCONF session, and also close the transport."
|
||||
try:
|
||||
return self._request(new_ele("close-session"))
|
||||
finally:
|
||||
self.session.close()
|
||||
|
||||
|
||||
class KillSession(RPC):
|
||||
|
||||
"`kill-session` RPC."
|
||||
|
||||
def request(self, session_id):
|
||||
"""Force the termination of a NETCONF session (not the current one!)
|
||||
|
||||
*session_id* is the session identifier of the NETCONF session to be terminated as a string
|
||||
"""
|
||||
node = new_ele("kill-session")
|
||||
sub_ele(node, "session-id").text = session_id
|
||||
return self._request(node)
|
24
ryu/contrib/ncclient/operations/subscribe.py
Normal file
24
ryu/contrib/ncclient/operations/subscribe.py
Normal file
@ -0,0 +1,24 @@
|
||||
# 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.
|
||||
|
||||
# TODO
|
||||
|
||||
class Notification:
|
||||
pass
|
||||
|
||||
class CreateSubscription:
|
||||
pass
|
||||
|
||||
class NotificationListener:
|
||||
pass
|
65
ryu/contrib/ncclient/operations/util.py
Normal file
65
ryu/contrib/ncclient/operations/util.py
Normal file
@ -0,0 +1,65 @@
|
||||
# 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.
|
||||
|
||||
'Boilerplate ugliness'
|
||||
|
||||
from ncclient.xml_ import *
|
||||
|
||||
from errors import OperationError, MissingCapabilityError
|
||||
|
||||
def one_of(*args):
|
||||
"Verifies that only one of the arguments is not None"
|
||||
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 datastore_or_url(wha, loc, capcheck=None):
|
||||
node = new_ele(wha)
|
||||
if "://" in loc: # e.g. http://, file://, ftp://
|
||||
if capcheck is not None:
|
||||
capcheck(":url") # url schema check at some point!
|
||||
sub_ele(node, "url").text = loc
|
||||
else:
|
||||
#if loc == 'candidate':
|
||||
# capcheck(':candidate')
|
||||
#elif loc == 'startup':
|
||||
# capcheck(':startup')
|
||||
#elif loc == 'running' and wha == 'target':
|
||||
# capcheck(':writable-running')
|
||||
sub_ele(node, loc)
|
||||
return node
|
||||
|
||||
def build_filter(spec, capcheck=None):
|
||||
type = None
|
||||
if isinstance(spec, tuple):
|
||||
type, criteria = spec
|
||||
rep = new_ele("filter", type=type)
|
||||
if type == "xpath":
|
||||
rep.attrib["select"] = criteria
|
||||
elif type == "subtree":
|
||||
rep.append(to_ele(criteria))
|
||||
else:
|
||||
raise OperationError("Invalid filter type")
|
||||
else:
|
||||
rep = validated_element(spec, ("filter", qualify("filter")),
|
||||
attrs=("type",))
|
||||
# TODO set type var here, check if select attr present in case of xpath..
|
||||
if type == "xpath" and capcheck is not None:
|
||||
capcheck(":xpath")
|
||||
return rep
|
30
ryu/contrib/ncclient/transport/__init__.py
Normal file
30
ryu/contrib/ncclient/transport/__init__.py
Normal file
@ -0,0 +1,30 @@
|
||||
# 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.
|
||||
|
||||
"Transport layer"
|
||||
|
||||
from session import Session, SessionListener
|
||||
from ssh import SSHSession
|
||||
from errors import *
|
||||
|
||||
__all__ = [
|
||||
'Session',
|
||||
'SessionListener',
|
||||
'SSHSession',
|
||||
'TransportError',
|
||||
'AuthenticationError',
|
||||
'SessionCloseError',
|
||||
'SSHError',
|
||||
'SSHUnknownHostError'
|
||||
]
|
41
ryu/contrib/ncclient/transport/errors.py
Normal file
41
ryu/contrib/ncclient/transport/errors.py
Normal file
@ -0,0 +1,41 @@
|
||||
# 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 NCClientError
|
||||
|
||||
class TransportError(NCClientError):
|
||||
pass
|
||||
|
||||
class AuthenticationError(TransportError):
|
||||
pass
|
||||
|
||||
class SessionCloseError(TransportError):
|
||||
|
||||
def __init__(self, in_buf, out_buf=None):
|
||||
msg = 'Unexpected session close'
|
||||
if in_buf:
|
||||
msg += '\nIN_BUFFER: `%s`' % in_buf
|
||||
if out_buf:
|
||||
msg += ' OUT_BUFFER: `%s`' % out_buf
|
||||
SSHError.__init__(self, msg)
|
||||
|
||||
class SSHError(TransportError):
|
||||
pass
|
||||
|
||||
class SSHUnknownHostError(SSHError):
|
||||
|
||||
def __init__(self, host, fingerprint):
|
||||
SSHError.__init__(self, 'Unknown host key [%s] for [%s]' % (fingerprint, host))
|
||||
self.host = host
|
||||
self.fingerprint = fingerprint
|
229
ryu/contrib/ncclient/transport/session.py
Normal file
229
ryu/contrib/ncclient/transport/session.py
Normal file
@ -0,0 +1,229 @@
|
||||
# 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 Queue import Queue
|
||||
from threading import Thread, Lock, Event
|
||||
|
||||
from ncclient.xml_ import *
|
||||
from ncclient.capabilities import Capabilities
|
||||
|
||||
from errors import TransportError
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger('ncclient.transport.session')
|
||||
|
||||
class Session(Thread):
|
||||
|
||||
"Base class for use by transport protocol implementations."
|
||||
|
||||
def __init__(self, capabilities):
|
||||
Thread.__init__(self)
|
||||
self.setDaemon(True)
|
||||
self._listeners = set()
|
||||
self._lock = Lock()
|
||||
self.setName('session')
|
||||
self._q = Queue()
|
||||
self._client_capabilities = capabilities
|
||||
self._server_capabilities = None # yet
|
||||
self._id = None # session-id
|
||||
self._connected = False # to be set/cleared by subclass implementation
|
||||
logger.debug('%r created: client_capabilities=%r' %
|
||||
(self, self._client_capabilities))
|
||||
|
||||
def _dispatch_message(self, 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:
|
||||
logger.debug('dispatching message to %r: %s' % (l, raw))
|
||||
l.callback(root, raw) # no try-except; fail loudly if you must!
|
||||
|
||||
def _dispatch_error(self, err):
|
||||
with self._lock:
|
||||
listeners = list(self._listeners)
|
||||
for l in listeners:
|
||||
logger.debug('dispatching error to %r' % l)
|
||||
try: # here we can be more considerate with catching exceptions
|
||||
l.errback(err)
|
||||
except Exception as e:
|
||||
logger.warning('error dispatching to %r: %r' % (l, e))
|
||||
|
||||
def _post_connect(self):
|
||||
"Greeting stuff"
|
||||
init_event = Event()
|
||||
error = [None] # so that err_cb can bind error[0]. just how it is.
|
||||
# callbacks
|
||||
def ok_cb(id, capabilities):
|
||||
self._id = id
|
||||
self._server_capabilities = capabilities
|
||||
init_event.set()
|
||||
def err_cb(err):
|
||||
error[0] = err
|
||||
init_event.set()
|
||||
listener = HelloHandler(ok_cb, err_cb)
|
||||
self.add_listener(listener)
|
||||
self.send(HelloHandler.build(self._client_capabilities))
|
||||
logger.debug('starting main loop')
|
||||
self.start()
|
||||
# we expect server's hello message
|
||||
init_event.wait()
|
||||
# received hello message or an error happened
|
||||
self.remove_listener(listener)
|
||||
if error[0]:
|
||||
raise error[0]
|
||||
#if ':base:1.0' not in self.server_capabilities:
|
||||
# raise MissingCapabilityError(':base:1.0')
|
||||
logger.info('initialized: session-id=%s | server_capabilities=%s' %
|
||||
(self._id, self._server_capabilities))
|
||||
|
||||
def add_listener(self, listener):
|
||||
"""Register a listener that will be notified of incoming messages and
|
||||
errors.
|
||||
|
||||
:type listener: :class:`SessionListener`
|
||||
"""
|
||||
logger.debug('installing listener %r' % listener)
|
||||
if not isinstance(listener, SessionListener):
|
||||
raise SessionError("Listener must be a SessionListener type")
|
||||
with self._lock:
|
||||
self._listeners.add(listener)
|
||||
|
||||
def remove_listener(self, listener):
|
||||
"""Unregister some listener; ignore if the listener was never
|
||||
registered.
|
||||
|
||||
:type listener: :class:`SessionListener`
|
||||
"""
|
||||
logger.debug('discarding listener %r' % listener)
|
||||
with self._lock:
|
||||
self._listeners.discard(listener)
|
||||
|
||||
def get_listener_instance(self, cls):
|
||||
"""If a listener of the specified type is registered, returns the
|
||||
instance.
|
||||
|
||||
:type cls: :class:`SessionListener`
|
||||
"""
|
||||
with self._lock:
|
||||
for listener in self._listeners:
|
||||
if isinstance(listener, cls):
|
||||
return listener
|
||||
|
||||
def connect(self, *args, **kwds): # subclass implements
|
||||
raise NotImplementedError
|
||||
|
||||
def run(self): # subclass implements
|
||||
raise NotImplementedError
|
||||
|
||||
def send(self, message):
|
||||
"""Send the supplied *message* (xml string) to NETCONF server."""
|
||||
if not self.connected:
|
||||
raise TransportError('Not connected to NETCONF server')
|
||||
logger.debug('queueing %s' % message)
|
||||
self._q.put(message)
|
||||
|
||||
### Properties
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"Connection status of the session."
|
||||
return self._connected
|
||||
|
||||
@property
|
||||
def client_capabilities(self):
|
||||
"Client's :class:`Capabilities`"
|
||||
return self._client_capabilities
|
||||
|
||||
@property
|
||||
def server_capabilities(self):
|
||||
"Server's :class:`Capabilities`"
|
||||
return self._server_capabilities
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""A string representing the `session-id`. If the session has not been initialized it will be `None`"""
|
||||
return self._id
|
||||
|
||||
|
||||
class SessionListener(object):
|
||||
|
||||
"""Base class for :class:`Session` listeners, which are notified when a new
|
||||
NETCONF message is received or an error occurs.
|
||||
|
||||
.. note::
|
||||
Avoid time-intensive tasks in a callback's context.
|
||||
"""
|
||||
|
||||
def callback(self, root, raw):
|
||||
"""Called when a new XML document is received. The *root* argument allows the callback to determine whether it wants to further process the document.
|
||||
|
||||
Here, *root* is a tuple of *(tag, attributes)* where *tag* is the qualified name of the root element and *attributes* is a dictionary of its attributes (also qualified names).
|
||||
|
||||
*raw* will contain the XML document as a string.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def errback(self, ex):
|
||||
"""Called when an error occurs.
|
||||
|
||||
:type ex: :exc:`Exception`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HelloHandler(SessionListener):
|
||||
|
||||
def __init__(self, init_cb, error_cb):
|
||||
self._init_cb = init_cb
|
||||
self._error_cb = error_cb
|
||||
|
||||
def callback(self, root, raw):
|
||||
tag, attrs = root
|
||||
if (tag == qualify("hello")) or (tag == "hello"):
|
||||
try:
|
||||
id, capabilities = HelloHandler.parse(raw)
|
||||
except Exception as e:
|
||||
self._error_cb(e)
|
||||
else:
|
||||
self._init_cb(id, capabilities)
|
||||
|
||||
def errback(self, err):
|
||||
self._error_cb(err)
|
||||
|
||||
@staticmethod
|
||||
def build(capabilities):
|
||||
"Given a list of capability URI's returns <hello> message XML string"
|
||||
hello = new_ele("hello")
|
||||
caps = sub_ele(hello, "capabilities")
|
||||
def fun(uri): sub_ele(caps, "capability").text = uri
|
||||
map(fun, capabilities)
|
||||
return to_xml(hello)
|
||||
|
||||
@staticmethod
|
||||
def parse(raw):
|
||||
"Returns tuple of (session-id (str), capabilities (Capabilities)"
|
||||
sid, capabilities = 0, []
|
||||
root = to_ele(raw)
|
||||
for child in root.getchildren():
|
||||
if child.tag == qualify("session-id") or child.tag == "session-id":
|
||||
sid = child.text
|
||||
elif child.tag == qualify("capabilities") or child.tag == "capabilities" :
|
||||
for cap in child.getchildren():
|
||||
if cap.tag == qualify("capability") or cap.tag == "capability":
|
||||
capabilities.append(cap.text)
|
||||
return sid, Capabilities(capabilities)
|
312
ryu/contrib/ncclient/transport/ssh.py
Normal file
312
ryu/contrib/ncclient/transport/ssh.py
Normal file
@ -0,0 +1,312 @@
|
||||
# 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 os
|
||||
import socket
|
||||
import getpass
|
||||
from binascii import hexlify
|
||||
from cStringIO import StringIO
|
||||
from select import select
|
||||
|
||||
import paramiko
|
||||
|
||||
from errors import AuthenticationError, SessionCloseError, SSHError, SSHUnknownHostError
|
||||
from session import Session
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger("ncclient.transport.ssh")
|
||||
|
||||
BUF_SIZE = 4096
|
||||
MSG_DELIM = "]]>]]>"
|
||||
TICK = 0.1
|
||||
|
||||
def default_unknown_host_cb(host, fingerprint):
|
||||
"""An unknown host callback returns `True` if it finds the key acceptable, and `False` if not.
|
||||
|
||||
This default callback always returns `False`, which would lead to :meth:`connect` raising a :exc:`SSHUnknownHost` exception.
|
||||
|
||||
Supply another valid callback if you need to verify the host key programatically.
|
||||
|
||||
*host* is the hostname that needs to be verified
|
||||
|
||||
*fingerprint* is a hex string representing the host key fingerprint, colon-delimited e.g. `"4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21"`
|
||||
"""
|
||||
return False
|
||||
|
||||
def _colonify(fp):
|
||||
finga = fp[:2]
|
||||
for idx in range(2, len(fp), 2):
|
||||
finga += ":" + fp[idx:idx+2]
|
||||
return finga
|
||||
|
||||
class SSHSession(Session):
|
||||
|
||||
"Implements a :rfc:`4742` NETCONF session over SSH."
|
||||
|
||||
def __init__(self, capabilities):
|
||||
Session.__init__(self, capabilities)
|
||||
self._host_keys = paramiko.HostKeys()
|
||||
self._transport = None
|
||||
self._connected = False
|
||||
self._channel = None
|
||||
self._buffer = StringIO() # for incoming data
|
||||
# parsing-related, see _parse()
|
||||
self._parsing_state = 0
|
||||
self._parsing_pos = 0
|
||||
|
||||
def _parse(self):
|
||||
"Messages ae delimited by MSG_DELIM. The buffer could have grown by a maximum of BUF_SIZE bytes everytime this method is called. Retains state across method calls and if a byte has been read it will not be considered again."
|
||||
delim = MSG_DELIM
|
||||
n = len(delim) - 1
|
||||
expect = self._parsing_state
|
||||
buf = self._buffer
|
||||
buf.seek(self._parsing_pos)
|
||||
while True:
|
||||
x = buf.read(1)
|
||||
if not x: # done reading
|
||||
break
|
||||
elif x == delim[expect]: # what we expected
|
||||
expect += 1 # expect the next delim char
|
||||
else:
|
||||
expect = 0
|
||||
continue
|
||||
# loop till last delim char expected, break if other char encountered
|
||||
for i in range(expect, n):
|
||||
x = buf.read(1)
|
||||
if not x: # done reading
|
||||
break
|
||||
if x == delim[expect]: # what we expected
|
||||
expect += 1 # expect the next delim char
|
||||
else:
|
||||
expect = 0 # reset
|
||||
break
|
||||
else: # if we didn't break out of the loop, full delim was parsed
|
||||
msg_till = buf.tell() - n
|
||||
buf.seek(0)
|
||||
logger.debug('parsed new message')
|
||||
self._dispatch_message(buf.read(msg_till).strip())
|
||||
buf.seek(n+1, os.SEEK_CUR)
|
||||
rest = buf.read()
|
||||
buf = StringIO()
|
||||
buf.write(rest)
|
||||
buf.seek(0)
|
||||
expect = 0
|
||||
self._buffer = buf
|
||||
self._parsing_state = expect
|
||||
self._parsing_pos = self._buffer.tell()
|
||||
|
||||
def load_known_hosts(self, filename=None):
|
||||
"""Load host keys from an openssh :file:`known_hosts`-style file. Can be called multiple times.
|
||||
|
||||
If *filename* is not specified, looks in the default locations i.e. :file:`~/.ssh/known_hosts` and :file:`~/ssh/known_hosts` for Windows.
|
||||
"""
|
||||
if filename is None:
|
||||
filename = os.path.expanduser('~/.ssh/known_hosts')
|
||||
try:
|
||||
self._host_keys.load(filename)
|
||||
except IOError:
|
||||
# for windows
|
||||
filename = os.path.expanduser('~/ssh/known_hosts')
|
||||
try:
|
||||
self._host_keys.load(filename)
|
||||
except IOError:
|
||||
pass
|
||||
else:
|
||||
self._host_keys.load(filename)
|
||||
|
||||
def close(self):
|
||||
if self._transport.is_active():
|
||||
self._transport.close()
|
||||
self._connected = False
|
||||
|
||||
# REMEMBER to update transport.rst if sig. changes, since it is hardcoded there
|
||||
def connect(self, host, port=830, timeout=None, unknown_host_cb=default_unknown_host_cb,
|
||||
username=None, password=None, key_filename=None, allow_agent=True, look_for_keys=True):
|
||||
"""Connect via SSH and initialize the NETCONF session. First attempts the publickey authentication method and then password authentication.
|
||||
|
||||
To disable attempting publickey authentication altogether, call with *allow_agent* and *look_for_keys* as `False`.
|
||||
|
||||
*host* is the hostname or IP address to connect to
|
||||
|
||||
*port* is by default 830, but some devices use the default SSH port of 22 so this may need to be specified
|
||||
|
||||
*timeout* is an optional timeout for socket connect
|
||||
|
||||
*unknown_host_cb* is called when the server host key is not recognized. It takes two arguments, the hostname and the fingerprint (see the signature of :func:`default_unknown_host_cb`)
|
||||
|
||||
*username* is the username to use for SSH authentication
|
||||
|
||||
*password* is the password used if using password authentication, or the passphrase to use for unlocking keys that require it
|
||||
|
||||
*key_filename* is a filename where a the private key to be used can be found
|
||||
|
||||
*allow_agent* enables querying SSH agent (if found) for keys
|
||||
|
||||
*look_for_keys* enables looking in the usual locations for ssh keys (e.g. :file:`~/.ssh/id_*`)
|
||||
"""
|
||||
if username is None:
|
||||
username = getpass.getuser()
|
||||
|
||||
sock = None
|
||||
for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
try:
|
||||
sock = socket.socket(af, socktype, proto)
|
||||
sock.settimeout(timeout)
|
||||
except socket.error:
|
||||
continue
|
||||
try:
|
||||
sock.connect(sa)
|
||||
except socket.error:
|
||||
sock.close()
|
||||
continue
|
||||
break
|
||||
else:
|
||||
raise SSHError("Could not open socket to %s:%s" % (host, port))
|
||||
|
||||
t = self._transport = paramiko.Transport(sock)
|
||||
t.set_log_channel(logger.name)
|
||||
|
||||
try:
|
||||
t.start_client()
|
||||
except paramiko.SSHException:
|
||||
raise SSHError('Negotiation failed')
|
||||
|
||||
# host key verification
|
||||
server_key = t.get_remote_server_key()
|
||||
known_host = self._host_keys.check(host, server_key)
|
||||
|
||||
fingerprint = _colonify(hexlify(server_key.get_fingerprint()))
|
||||
|
||||
if not known_host and not unknown_host_cb(host, fingerprint):
|
||||
raise SSHUnknownHostError(host, fingerprint)
|
||||
|
||||
if key_filename is None:
|
||||
key_filenames = []
|
||||
elif isinstance(key_filename, basestring):
|
||||
key_filenames = [ key_filename ]
|
||||
else:
|
||||
key_filenames = key_filename
|
||||
|
||||
self._auth(username, password, key_filenames, allow_agent, look_for_keys)
|
||||
|
||||
self._connected = True # there was no error authenticating
|
||||
|
||||
c = self._channel = self._transport.open_session()
|
||||
c.set_name("netconf")
|
||||
c.invoke_subsystem("netconf")
|
||||
|
||||
self._post_connect()
|
||||
|
||||
# on the lines of paramiko.SSHClient._auth()
|
||||
def _auth(self, username, password, key_filenames, allow_agent,
|
||||
look_for_keys):
|
||||
saved_exception = None
|
||||
|
||||
for key_filename in key_filenames:
|
||||
for cls in (paramiko.RSAKey, paramiko.DSSKey):
|
||||
try:
|
||||
key = cls.from_private_key_file(key_filename, password)
|
||||
logger.debug("Trying key %s from %s" %
|
||||
(hexlify(key.get_fingerprint()), key_filename))
|
||||
self._transport.auth_publickey(username, key)
|
||||
return
|
||||
except Exception as e:
|
||||
saved_exception = e
|
||||
logger.debug(e)
|
||||
|
||||
if allow_agent:
|
||||
for key in paramiko.Agent().get_keys():
|
||||
try:
|
||||
logger.debug("Trying SSH agent key %s" %
|
||||
hexlify(key.get_fingerprint()))
|
||||
self._transport.auth_publickey(username, key)
|
||||
return
|
||||
except Exception as e:
|
||||
saved_exception = e
|
||||
logger.debug(e)
|
||||
|
||||
keyfiles = []
|
||||
if look_for_keys:
|
||||
rsa_key = os.path.expanduser("~/.ssh/id_rsa")
|
||||
dsa_key = os.path.expanduser("~/.ssh/id_dsa")
|
||||
if os.path.isfile(rsa_key):
|
||||
keyfiles.append((paramiko.RSAKey, rsa_key))
|
||||
if os.path.isfile(dsa_key):
|
||||
keyfiles.append((paramiko.DSSKey, dsa_key))
|
||||
# look in ~/ssh/ for windows users:
|
||||
rsa_key = os.path.expanduser("~/ssh/id_rsa")
|
||||
dsa_key = os.path.expanduser("~/ssh/id_dsa")
|
||||
if os.path.isfile(rsa_key):
|
||||
keyfiles.append((paramiko.RSAKey, rsa_key))
|
||||
if os.path.isfile(dsa_key):
|
||||
keyfiles.append((paramiko.DSSKey, dsa_key))
|
||||
|
||||
for cls, filename in keyfiles:
|
||||
try:
|
||||
key = cls.from_private_key_file(filename, password)
|
||||
logger.debug("Trying discovered key %s in %s" %
|
||||
(hexlify(key.get_fingerprint()), filename))
|
||||
self._transport.auth_publickey(username, key)
|
||||
return
|
||||
except Exception as e:
|
||||
saved_exception = e
|
||||
logger.debug(e)
|
||||
|
||||
if password is not None:
|
||||
try:
|
||||
self._transport.auth_password(username, password)
|
||||
return
|
||||
except Exception as e:
|
||||
saved_exception = e
|
||||
logger.debug(e)
|
||||
|
||||
if saved_exception is not None:
|
||||
# need pep-3134 to do this right
|
||||
raise AuthenticationError(repr(saved_exception))
|
||||
|
||||
raise AuthenticationError("No authentication methods available")
|
||||
|
||||
def run(self):
|
||||
chan = self._channel
|
||||
q = self._q
|
||||
try:
|
||||
while True:
|
||||
# select on a paramiko ssh channel object does not ever return it in the writable list, so channels don't exactly emulate the socket api
|
||||
r, w, e = select([chan], [], [], TICK)
|
||||
# will wakeup evey TICK seconds to check if something to send, more if something to read (due to select returning chan in readable list)
|
||||
if r:
|
||||
data = chan.recv(BUF_SIZE)
|
||||
if data:
|
||||
self._buffer.write(data)
|
||||
self._parse()
|
||||
else:
|
||||
raise SessionCloseError(self._buffer.getvalue())
|
||||
if not q.empty() and chan.send_ready():
|
||||
logger.debug("Sending message")
|
||||
data = q.get() + MSG_DELIM
|
||||
while data:
|
||||
n = chan.send(data)
|
||||
if n <= 0:
|
||||
raise SessionCloseError(self._buffer.getvalue(), data)
|
||||
data = data[n:]
|
||||
except Exception as e:
|
||||
logger.debug("Broke out of main loop, error=%r", e)
|
||||
self.close()
|
||||
self._dispatch_error(e)
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
"Underlying `paramiko.Transport <http://www.lag.net/paramiko/docs/paramiko.Transport-class.html>`_ object. This makes it possible to call methods like :meth:`~paramiko.Transport.set_keepalive` on it."
|
||||
return self._transport
|
108
ryu/contrib/ncclient/xml_.py
Normal file
108
ryu/contrib/ncclient/xml_.py
Normal file
@ -0,0 +1,108 @@
|
||||
# Copyright 2009 Shikhar Bhushan
|
||||
# Copyright 2011 Leonidas Poulopoulos
|
||||
#
|
||||
# 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.
|
||||
|
||||
"Methods for creating, parsing, and dealing with XML and ElementTree objects."
|
||||
|
||||
from cStringIO import StringIO
|
||||
from xml.etree import cElementTree as ET
|
||||
|
||||
# In case issues come up with XML generation/parsing
|
||||
# make sure you have the ElementTree v1.2.7+ lib
|
||||
|
||||
from ncclient import NCClientError
|
||||
|
||||
class XMLError(NCClientError): pass
|
||||
|
||||
### Namespace-related
|
||||
|
||||
#: Base NETCONF namespace
|
||||
BASE_NS_1_0 = "urn:ietf:params:xml:ns:netconf:base:1.0"
|
||||
#: Namespace for Tail-f core data model
|
||||
TAILF_AAA_1_1 = "http://tail-f.com/ns/aaa/1.1"
|
||||
#: Namespace for Tail-f execd data model
|
||||
TAILF_EXECD_1_1 = "http://tail-f.com/ns/execd/1.1"
|
||||
#: Namespace for Cisco data model
|
||||
CISCO_CPI_1_0 = "http://www.cisco.com/cpi_10/schema"
|
||||
#: Namespace for Flowmon data model
|
||||
FLOWMON_1_0 = "http://www.liberouter.org/ns/netopeer/flowmon/1.0"
|
||||
#: Namespace for Juniper 9.6R4. Tested with Junos 9.6R4+
|
||||
JUNIPER_1_1 = "http://xml.juniper.net/xnm/1.1/xnm"
|
||||
#
|
||||
try:
|
||||
register_namespace = ET.register_namespace
|
||||
except AttributeError:
|
||||
def register_namespace(prefix, uri):
|
||||
from xml.etree import ElementTree
|
||||
# cElementTree uses ElementTree's _namespace_map, so that's ok
|
||||
ElementTree._namespace_map[uri] = prefix
|
||||
register_namespace.func_doc = "ElementTree's namespace map determines the prefixes for namespace URI's when serializing to XML. This method allows modifying this map to specify a prefix for a namespace URI."
|
||||
|
||||
for (ns, pre) in {
|
||||
BASE_NS_1_0: 'nc',
|
||||
TAILF_AAA_1_1: 'aaa',
|
||||
TAILF_EXECD_1_1: 'execd',
|
||||
CISCO_CPI_1_0: 'cpi',
|
||||
FLOWMON_1_0: 'fm',
|
||||
JUNIPER_1_1: 'junos',
|
||||
}.items():
|
||||
register_namespace(pre, ns)
|
||||
|
||||
qualify = lambda tag, ns=BASE_NS_1_0: tag if ns is None else "{%s}%s" % (ns, tag)
|
||||
"""Qualify a *tag* name with a *namespace*, in :mod:`~xml.etree.ElementTree` fashion i.e. *{namespace}tagname*."""
|
||||
|
||||
def to_xml(ele, encoding="UTF-8"):
|
||||
"Convert and return the XML for an *ele* (:class:`~xml.etree.ElementTree.Element`) with specified *encoding*."
|
||||
xml = ET.tostring(ele, encoding)
|
||||
return xml if xml.startswith('<?xml') else '<?xml version="1.0" encoding="%s"?>%s' % (encoding, xml)
|
||||
|
||||
def to_ele(x):
|
||||
"Convert and return the :class:`~xml.etree.ElementTree.Element` for the XML document *x*. If *x* is already an :class:`~xml.etree.ElementTree.Element` simply returns that."
|
||||
return x if ET.iselement(x) else ET.fromstring(x)
|
||||
|
||||
def parse_root(raw):
|
||||
"Efficiently parses the root element of a *raw* XML document, returning a tuple of its qualified name and attribute dictionary."
|
||||
fp = StringIO(raw)
|
||||
for event, element in ET.iterparse(fp, events=('start',)):
|
||||
return (element.tag, element.attrib)
|
||||
|
||||
def validated_element(x, tags=None, attrs=None):
|
||||
"""Checks if the root element of an XML document or Element meets the supplied criteria.
|
||||
|
||||
*tags* if specified is either a single allowable tag name or sequence of allowable alternatives
|
||||
|
||||
*attrs* if specified is a sequence of required attributes, each of which may be a sequence of several allowable alternatives
|
||||
|
||||
Raises :exc:`XMLError` if the requirements are not met.
|
||||
"""
|
||||
ele = to_ele(x)
|
||||
if tags:
|
||||
if isinstance(tags, basestring):
|
||||
tags = [tags]
|
||||
if ele.tag not in tags:
|
||||
raise XMLError("Element [%s] does not meet requirement" % ele.tag)
|
||||
if attrs:
|
||||
for req in attrs:
|
||||
if isinstance(req, basestring): req = [req]
|
||||
for alt in req:
|
||||
if alt in ele.attrib:
|
||||
break
|
||||
else:
|
||||
raise XMLError("Element [%s] does not have required attributes" % ele.tag)
|
||||
return ele
|
||||
|
||||
new_ele = lambda tag, attrs={}, **extra: ET.Element(qualify(tag), attrs, **extra)
|
||||
|
||||
sub_ele = lambda parent, tag, attrs={}, **extra: ET.SubElement(parent, qualify(tag), attrs, **extra)
|
||||
|
@ -6,7 +6,7 @@ source-dir = doc/source
|
||||
[bdist_rpm]
|
||||
Release = 1
|
||||
Group = Applications/Accessories
|
||||
Requires = python-gevent >= 0.13, python-routes, python-webob
|
||||
Requires = python-gevent >= 0.13, python-routes, python-webob, python-paramiko
|
||||
doc_files = LICENSE
|
||||
MANIFEST.in
|
||||
README.rst
|
||||
|
@ -1,3 +1,4 @@
|
||||
gevent>=0.13
|
||||
routes
|
||||
webob>=1.0.8
|
||||
paramiko
|
||||
|
Loading…
x
Reference in New Issue
Block a user