pyIOSXR-0.52/ 0000755 0001750 0001750 00000000000 13135644413 013563 5 ustar travis travis 0000000 0000000 pyIOSXR-0.52/pyIOSXR/ 0000755 0001750 0001750 00000000000 13135644413 015040 5 ustar travis travis 0000000 0000000 pyIOSXR-0.52/pyIOSXR/__init__.py 0000644 0001750 0001750 00000001441 13135644353 017154 0 ustar travis travis 0000000 0000000 #!/usr/bin/env python
# coding=utf-8
"""A module to interact with Cisco devices running IOS-XR."""
# Copyright 2015 Netflix. All rights reserved.
# Copyright 2016 BigWaveIT. All rights reserved.
#
# The contents of this file are 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 pyIOSXR.iosxr import IOSXR
pyIOSXR-0.52/pyIOSXR/exceptions.py 0000644 0001750 0001750 00000005156 13135644353 017605 0 ustar travis travis 0000000 0000000 #!/usr/bin/env python
# coding=utf-8
"""Exceptions for pyiosxr, a module to interact with Cisco devices running IOS-XR."""
# Copyright 2015 Netflix. All rights reserved.
# Copyright 2016 BigWaveIT. All rights reserved.
#
# The contents of this file are 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.
class IOSXRException(Exception):
def __init__(self, msg=None, dev=None):
super(IOSXRException, self).__init__(msg)
if dev:
self._xr = dev
# release the XML agent
if self._xr._xml_agent_locker.locked():
self._xr._xml_agent_locker.release()
class ConnectError(IOSXRException):
"""Exception while openning the connection."""
def __init__(self, msg=None, dev=None):
super(ConnectError, self).__init__(msg=msg, dev=dev)
if dev:
self._xr = dev
self._xr._xml_agent_alive = False
class CommitError(IOSXRException):
"""Raised when unable to commit. Mostly due to ERROR 0x41866c00"""
pass
class LockError(IOSXRException):
"""Throw this exception when unable to lock the config DB."""
pass
class UnlockError(IOSXRException):
"""Throw this exception when unable to unlock the config DB."""
pass
class CompareConfigError(IOSXRException):
"""Throw this exception when unable to compare config."""
pass
class UnknownError(IOSXRException):
"""UnknownError Exception."""
pass
class InvalidInputError(IOSXRException):
"""InvalidInputError Exception."""
pass
class XMLCLIError(IOSXRException):
"""XMLCLIError Exception."""
pass
class InvalidXMLResponse(IOSXRException):
"""Raised when unable to process properly the XML reply from the device."""
pass
class TimeoutError(IOSXRException):
"""TimeoutError Exception."""
def __init__(self, msg=None, dev=None):
super(TimeoutError, self).__init__(msg=msg, dev=dev)
if dev:
self._xr = dev
self._xr._xml_agent_alive = False
class EOFError(IOSXRException):
"""EOFError Exception."""
pass
class IteratorIDError(IOSXRException):
"""IteratorIDError Exception."""
pass
pyIOSXR-0.52/pyIOSXR/iosxr.py 0000644 0001750 0001750 00000063647 13135644353 016601 0 ustar travis travis 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright 2015 Netflix. All rights reserved.
# Copyright 2016 BigWaveIT. All rights reserved.
#
# The contents of this file are 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.
"""Contains the main IOS-XR driver class."""
# stdlib
import re
import time
import difflib
from threading import Lock
from xml.sax.saxutils import escape as escape_xml
# third party lib
from lxml import etree as ET
from netmiko import ConnectHandler
from netmiko.ssh_exception import NetMikoTimeoutException
from netmiko.ssh_exception import NetMikoAuthenticationException
# local modules
from pyIOSXR.exceptions import LockError
from pyIOSXR.exceptions import UnlockError
from pyIOSXR.exceptions import XMLCLIError
from pyIOSXR.exceptions import CommitError
from pyIOSXR.exceptions import ConnectError
from pyIOSXR.exceptions import TimeoutError
from pyIOSXR.exceptions import IteratorIDError
from pyIOSXR.exceptions import InvalidInputError
from pyIOSXR.exceptions import CompareConfigError
from pyIOSXR.exceptions import InvalidXMLResponse
class IOSXR(object):
"""
Establishes a connection with the IOS-XR device via SSH and facilitates the communication through the XML agent.
"""
_XML_SHELL = 'xml'
_XML_MODE_PROMPT = r'XML>'
_READ_DELAY = 0.1 # at least 0.1, corresponding to 600 max loops (60s timeout)
_XML_MODE_DELAY = 1 # should be able to read within one second
_ITERATOR_ID_ERROR_MSG = (
'Non supported IteratorID in Response object.'
'Turn iteration off on your XML agent by configuring "xml agent [tty | ssl] iteration off".'
'For more information refer to'
'http://www.cisco.com/c/en/us/td/docs/ios_xr_sw/iosxr_r4-1/xml/programming/guide/xl41apidoc.pdf, 7-99.'
'Please turn iteration off for the XML agent.'
)
def __init__(self,
hostname,
username,
password,
port=22,
timeout=60,
logfile=None,
lock=True,
**netmiko_kwargs):
"""
IOS-XR device constructor.
:param hostname: (str) IP or FQDN of the target device
:param username: (str) Username
:param password: (str) Password
:param port: (int) SSH Port (default: 22)
:param timeout: (int) Timeout (default: 60 sec)
:param logfile: File-like object to save device communication to or None to disable logging
:param lock: (bool) Auto-lock config upon open() if set to True, connect without locking if False
(default: True)
:netmiko_kwargs (kwargs) Key-value args to forward to Netmiko.
"""
self.hostname = str(hostname)
self.username = str(username)
self.password = str(password)
self.port = int(port)
self.timeout = int(timeout)
self.logfile = logfile
self.lock_on_connect = lock
self.locked = False
self.netmiko_kwargs = netmiko_kwargs
self._cli_prompt = None
self._xml_agent_locker = Lock()
self._xml_agent_alive = False
def __getattr__(self, item):
"""
Dynamic getter to translate generic show commands.
David came up with this dynamic method. It takes
calls with show commands encoded in the name. I'll replace the
underscores for spaces and issues the show command on the device...
pretty neat!
non keyword params for show command:
all non keyword arguments is added to the command to allow dynamic parameters:
eg: .show_interface("GigabitEthernet0/0/0/0")
keyword params for show command:
config=True/False : set True to run show command in config mode
eg: .show_configuration_merge(config=True)
"""
def _getattr(*args, **kwargs):
cmd = item.replace('_', ' ')
for arg in args:
cmd += " %s" % arg
if kwargs.get("config"):
response = self._execute_config_show(cmd)
else:
response = self._execute_show(cmd)
match = re.search(".*(!! IOS XR Configuration.*)", response, re.DOTALL)
if match is not None:
response = match.group(1)
return response
if item.startswith('show'):
return _getattr
else:
raise AttributeError("type object '%s' has no attribute '%s'" % (self.__class__.__name__, item))
def make_rpc_call(self, rpc_command):
"""
Allow a user to query a device directly using XML-requests.
:param rpc_command: (str) rpc command such as:
"""
# ~~~ hack: ~~~
if not self.is_alive():
self.close() # force close for safety
self.open() # reopen
# ~~~ end hack ~~~
result = self._execute_rpc(rpc_command)
return ET.tostring(result)
def open(self):
"""
Open a connection to an IOS-XR device.
Connects to the device using SSH and drops into XML mode.
"""
try:
self.device = ConnectHandler(device_type='cisco_xr',
ip=self.hostname,
port=self.port,
username=self.username,
password=self.password,
**self.netmiko_kwargs)
self.device.timeout = self.timeout
self._xml_agent_alive = True # successfully open thus alive
except NetMikoTimeoutException as t_err:
raise ConnectError(t_err.args[0])
except NetMikoAuthenticationException as au_err:
raise ConnectError(au_err.args[0])
self._cli_prompt = self.device.find_prompt() # get the prompt
self._enter_xml_mode()
def is_alive(self):
"""
Returns the XML agent connection state (and SSH connection state).
"""
if hasattr(self.device, 'remote_conn'):
return self.device.remote_conn.transport.is_active() and self._xml_agent_alive
return False # remote_conn not there => connection not init => not alive
def _timeout_exceeded(self, start=None, msg='Timeout exceeded!'):
if not start:
return False # reference not specified, noth to compare => no error
if time.time() - start > self.timeout:
# it timeout exceeded, throw TimeoutError
raise TimeoutError(msg, self)
return False
def _lock_xml_agent(self, start=None):
while (not self._xml_agent_locker.acquire(False)
and not self._timeout_exceeded(start, 'Waiting to acquire the XML agent!')):
# will wait here till the XML agent is ready to receive new requests
# if stays too much, _timeout_exceeded will raise TimeoutError
pass # do nothing, just wait
return True # ready to go now
def _unlock_xml_agent(self):
if self._xml_agent_locker.locked():
self._xml_agent_locker.release()
def _send_command_timing(self, command):
return self.device.send_command_timing(command,
delay_factor=self._READ_DELAY,
max_loops=self._XML_MODE_DELAY/self._READ_DELAY,
strip_prompt=False,
strip_command=False)
def _in_cli_mode(self):
out = self._send_command_timing('\n')
if not out:
return False
if self._cli_prompt in out:
return True
return False
def _enter_xml_mode(self):
self._unlock_xml_agent()
# release - other commands should not have anyway access to the XML agent
# when not in XML mode
self._lock_xml_agent() # make sure it won't collide with other parallel requests
out = self._send_command_timing(self._XML_SHELL) # send xml shell command
if '0x24319600' in out:
# XML agent is not enabled
raise ConnectError('XML agent is not enabled. Please configure `xml agent tty iteration off`!', self)
self._unlock_xml_agent()
if self.lock_on_connect:
self.lock()
def _send_command(self,
command,
delay_factor=None,
start=None,
expect_string=None,
read_output=None,
receive=False):
if not expect_string:
expect_string = self._XML_MODE_PROMPT
if read_output is None:
read_output = ''
if not delay_factor:
delay_factor = self._READ_DELAY
if not start:
start = time.time()
output = read_output
last_read = ''
if not read_output and not receive:
# because the XML agent is able to process only one single request over the same SSH session at a time
# first come first served
self._lock_xml_agent(start)
try:
max_loops = self.timeout / delay_factor
last_read = self.device.send_command_expect(command,
expect_string=expect_string,
strip_prompt=False,
strip_command=False,
delay_factor=delay_factor,
max_loops=max_loops)
output += last_read
except IOError as ioe:
if ((not last_read and self._in_cli_mode()) or
(self._cli_prompt in output and "% Invalid input detected at '^' marker." in output)):
# something happened
# e.g. connection with the XML agent died while reading
# netmiko throws error and the last output read is empty (ofc)
# and in CLI mode
#
# OR
#
# Sometimes the XML agent simply exits and all issued commands provide the following output
# (as in CLI mode)
#
# ^
# % Invalid input detected at '^' marker.
# RP/0/RSP1/CPU0:edge01.dus01#'):
if '0x44318c06' in output or (self._cli_prompt and expect_string != self._cli_prompt and \
(output.startswith(self._cli_prompt) or output.endswith(self._cli_prompt))):
# sometimes the device throws a stupid error like:
# ERROR: 0x44318c06 'XML-TTY' detected the 'warning' condition
# 'A Light Weight Messaging library communication function returned an error': No such device or address
# and the XML agent connection is closed, but the SSH connection is fortunately maintained
# OR sometimes, the device simply exits from the XML mode without any clue
# In both cases, we need to re-enter in XML mode...
# so, whenever the CLI promt is detected, will re-enter in XML mode
# unless the expected string is the prompt
self._unlock_xml_agent()
self._enter_xml_mode()
# however, the command could not be executed properly, so we need to raise the XMLCLIError exception
raise XMLCLIError('Could not properly execute the command. Re-entering XML mode...', self)
if not output.strip(): # empty output, means that the device did not start delivering the output
# but for sure is still in XML mode as netmiko did not throw error
if not self._timeout_exceeded(start=start):
return self._send_command(command, receive=True, start=start) # let's try receiving more
raise XMLCLIError(output.strip(), self)
self._unlock_xml_agent()
return str(output.replace('XML>', '').strip())
def _netmiko_recv(self):
output = ''
for tmp_output in self.device.receive_data_generator():
output += tmp_output
return output
# previous module function __execute_rpc__
def _execute_rpc(self, command_xml, delay_factor=.1):
xml_rpc_command = '' \
+ command_xml + ''
response = self._send_command(xml_rpc_command, delay_factor=delay_factor)
try:
root = ET.fromstring(str.encode(response))
except ET.XMLSyntaxError as xml_err:
if 'IteratorID="' in response:
raise IteratorIDError(self._ITERATOR_ID_ERROR_MSG, self)
raise InvalidXMLResponse('Unable to process the XML Response from the device!', self)
if 'IteratorID' in root.attrib:
raise IteratorIDError(self._ITERATOR_ID_ERROR_MSG, self)
childs = [x.tag for x in list(root)]
result_summary = root.find('ResultSummary')
if result_summary is not None and int(result_summary.get('ErrorCount', 0)) > 0:
if 'CLI' in childs:
error_msg = root.find('CLI').get('ErrorMsg') or ''
elif 'Commit' in childs:
error_msg = root.find('Commit').get('ErrorMsg') or ''
error_code = root.find('Commit').get('ErrorCode') or ''
if error_code == '0x41866c00':
# yet another pointless IOS-XR error:
# if the config DB was changed by another process,
# while the current SSH connection is established and alive,
# we won't be able to commit and the device will throw the following error:
# 'CfgMgr' detected the 'warning' condition
# 'One or more commits have occurred from other configuration sessions since this session started
# or since the last commit was made from this session.'
# dumb.
# in this case we need to re-open the connection with the XML agent
_candidate_config = self.get_candidate_config(merge=True)
self.discard_config() # discard candidate config
try:
# exiting from the XML mode
self._send_command('exit', expect_string=self._cli_prompt)
except XMLCLIError:
pass # because does not end with `XML>`
self._enter_xml_mode() # re-entering XML mode
self.load_candidate_config(config=_candidate_config)
return self.commit_config()
elif error_code == '0x41864e00' or error_code == '0x43682c00':
# raises this error when the commit buffer is empty
raise CommitError('The target configuration buffer is empty.', self)
else:
error_msg = root.get('ErrorMsg') or ''
error_msg += '\nOriginal call was: %s' % xml_rpc_command
raise XMLCLIError(error_msg, self)
if 'CLI' in childs:
cli_childs = [x.tag for x in list(root.find('CLI'))]
if 'Configuration' in cli_childs:
output = root.find('CLI').find('Configuration').text
elif 'Exec' in cli_childs:
output = root.find('CLI').find('Exec').text
if output is None:
output = ''
elif 'Invalid input detected' in output:
raise InvalidInputError('Invalid input entered:\n%s' % output, self)
return root
# previous module function __execute_show__
def _execute_show(self, show_command):
"""
Executes an operational show-type command.
"""
rpc_command = '{show_command}'.format(
show_command=escape_xml(show_command)
)
response = self._execute_rpc(rpc_command)
raw_response = response.xpath('.//CLI/Exec')[0].text
return raw_response.strip() if raw_response else ''
# previous module function __execute_config_show__
def _execute_config_show(self, show_command, delay_factor=.1):
"""
Executes a configuration show-type command.
"""
rpc_command = '{show_command}'.format(
show_command=escape_xml(show_command)
)
response = self._execute_rpc(rpc_command, delay_factor=delay_factor)
raw_response = response.xpath('.//CLI/Configuration')[0].text
return raw_response.strip() if raw_response else ''
def close(self):
"""
Close the connection to the IOS-XR device.
Clean up after you are done and explicitly close the router connection.
"""
if self.lock_on_connect or self.locked:
self.unlock() # this refers to the config DB
self._unlock_xml_agent() # this refers to the XML agent
if hasattr(self.device, 'remote_conn'):
self.device.remote_conn.close() # close the underlying SSH session
def lock(self):
"""
Lock the config database.
Use if Locking/Unlocking is not performaed automatically by lock=False
"""
if not self.locked:
rpc_command = ''
try:
self._execute_rpc(rpc_command)
except XMLCLIError:
raise LockError('Unable to enter in configure exclusive mode!', self)
self.locked = True
def unlock(self):
"""
Unlock the IOS-XR device config.
Use if Locking/Unlocking is not performaed automatically by lock=False
"""
if self.locked:
rpc_command = ''
try:
self._execute_rpc(rpc_command)
except XMLCLIError:
raise UnlockError('Unable to unlock the config!', self)
self.locked = False
def load_candidate_config(self, filename=None, config=None):
"""
Load candidate confguration.
Populate the attribute candidate_config with the desired
configuration and loads it into the router. You can populate it from
a file or from a string. If you send both a filename and a string
containing the configuration, the file takes precedence.
:param filename: Path to the file containing the desired
configuration. By default is None.
:param config: String containing the desired configuration.
"""
configuration = ''
if filename is None:
configuration = config
else:
with open(filename) as f:
configuration = f.read()
rpc_command = '{configuration}'.format(
configuration=escape_xml(configuration) # need to escape, otherwise will try to load invalid XML
)
try:
self._execute_rpc(rpc_command)
except InvalidInputError as e:
self.discard_config()
raise InvalidInputError(e.args[0], self)
def get_candidate_config(self, merge=False, formal=False):
"""
Retrieve the configuration loaded as candidate config in your configuration session.
:param merge: Merge candidate config with running config to return
the complete configuration including all changed
:param formal: Return configuration in IOS-XR formal config format
"""
command = "show configuration"
if merge:
command += " merge"
if formal:
command += " formal"
response = self._execute_config_show(command)
match = re.search(".*(!! IOS XR Configuration.*)$", response, re.DOTALL)
if match is not None:
response = match.group(1)
return response
def compare_config(self):
"""
Compare configuration to be merged with the one on the device.
Compare executed candidate config with the running config and
return a diff, assuming the loaded config will be merged with the
existing one.
:return: Config diff.
"""
_show_merge = self._execute_config_show('show configuration merge')
_show_run = self._execute_config_show('show running-config')
diff = difflib.unified_diff(_show_run.splitlines(1)[2:-2], _show_merge.splitlines(1)[2:-2])
return ''.join([x.replace('\r', '') for x in diff])
def compare_replace_config(self):
"""
Compare configuration to be replaced with the one on the device.
Compare executed candidate config with the running config and
return a diff, assuming the entire config will be replaced.
:return: Config diff.
"""
diff = self._execute_config_show('show configuration changes diff')
return ''.join(diff.splitlines(1)[2:-2])
def commit_config(self, label=None, comment=None, confirmed=None):
"""
Commit the candidate config.
:param label: Commit comment, displayed in the commit entry on the device.
:param comment: Commit label, displayed instead of the commit ID on the device.
:param confirmed: Commit with auto-rollback if new commit is not made in 30 to 300 sec
"""
rpc_command = ''
self._execute_rpc(rpc_command)
def commit_replace_config(self, label=None, comment=None, confirmed=None):
"""
Commit the candidate config to the device, by replacing the existing one.
:param comment: User comment saved on this commit on the device
:param label: User label saved on this commit on the device
:param confirmed: Commit with auto-rollback if new commit is not made in 30 to 300 sec
"""
rpc_command = ''
self._execute_rpc(rpc_command)
def discard_config(self):
"""
Clear uncommited changes in the current session.
Clear previously loaded configuration on the device without committing it.
"""
rpc_command = ''
self._execute_rpc(rpc_command)
def rollback(self, rb_id=1):
"""
Rollback the last committed configuration.
:param rb_id: Rollback a specific number of steps. Default: 1
"""
rpc_command = '{rb_id}'.format(rb_id=rb_id)
self._execute_rpc(rpc_command)
pyIOSXR-0.52/pyIOSXR.egg-info/ 0000755 0001750 0001750 00000000000 13135644413 016532 5 ustar travis travis 0000000 0000000 pyIOSXR-0.52/pyIOSXR.egg-info/PKG-INFO 0000644 0001750 0001750 00000000644 13135644413 017633 0 ustar travis travis 0000000 0000000 Metadata-Version: 1.1
Name: pyIOSXR
Version: 0.52
Summary: Python API to interact with network devices running IOS-XR
Home-page: https://github.com/fooelisa/pyiosxr/
Author: Elisa Jasinska, Mircea Ulinic
Author-email: elisa@bigwaveit.org, mircea@cloudflare.com
License: UNKNOWN
Download-URL: https://github.com/fooelisa/pyiosxr/tarball/0.52
Description: UNKNOWN
Keywords: IOS-XR,IOSXR,Cisco,networking
Platform: UNKNOWN
pyIOSXR-0.52/pyIOSXR.egg-info/SOURCES.txt 0000644 0001750 0001750 00000000421 13135644413 020413 0 ustar travis travis 0000000 0000000 MANIFEST.in
requirements.txt
setup.cfg
setup.py
pyIOSXR/__init__.py
pyIOSXR/exceptions.py
pyIOSXR/iosxr.py
pyIOSXR.egg-info/PKG-INFO
pyIOSXR.egg-info/SOURCES.txt
pyIOSXR.egg-info/dependency_links.txt
pyIOSXR.egg-info/requires.txt
pyIOSXR.egg-info/top_level.txt
test/test.py pyIOSXR-0.52/pyIOSXR.egg-info/dependency_links.txt 0000644 0001750 0001750 00000000001 13135644413 022600 0 ustar travis travis 0000000 0000000
pyIOSXR-0.52/pyIOSXR.egg-info/requires.txt 0000644 0001750 0001750 00000000033 13135644413 021126 0 ustar travis travis 0000000 0000000 netmiko>=1.4.1
lxml>=3.2.4
pyIOSXR-0.52/pyIOSXR.egg-info/top_level.txt 0000644 0001750 0001750 00000000010 13135644413 021253 0 ustar travis travis 0000000 0000000 pyIOSXR
pyIOSXR-0.52/test/ 0000755 0001750 0001750 00000000000 13135644413 014542 5 ustar travis travis 0000000 0000000 pyIOSXR-0.52/test/test.py 0000755 0001750 0001750 00000054563 13135644353 016116 0 ustar travis travis 0000000 0000000 #!/usr/bin/env python
# coding=utf-8
"""Unit tests for pyiosxr, a module to interact with Cisco devices running IOS-XR."""
import os
import sys
import time
import unittest
from lxml import etree as ET
from six import binary_type
# ~~~ import pyIOSXR modules ~~~
from pyIOSXR import IOSXR
# exceptions
from pyIOSXR.exceptions import LockError
from pyIOSXR.exceptions import UnlockError
from pyIOSXR.exceptions import XMLCLIError
from pyIOSXR.exceptions import CommitError
from pyIOSXR.exceptions import ConnectError
from pyIOSXR.exceptions import TimeoutError
from pyIOSXR.exceptions import IteratorIDError
from pyIOSXR.exceptions import InvalidInputError
from pyIOSXR.exceptions import CompareConfigError
from pyIOSXR.exceptions import InvalidXMLResponse
class _MockedNetMikoDevice(object):
"""
Defines the minimum attributes necessary to mock a SSH connection using netmiko.
"""
def __init__(self):
class _MockedParamikoTransport(object):
def close(self):
pass
self.remote_conn = _MockedParamikoTransport()
@staticmethod
def get_mock_file(command, format='xml'):
filename = \
command.replace('', '')\
.replace('', '')\
.replace('<', '')\
.replace('>', '_')\
.replace('/', '')\
.replace('\n', '')\
.replace('.', '_')\
.replace(' ', '_')\
.replace('"', '_')\
.replace('=', '_')\
.replace('$', '')\
.replace(':', '')\
.replace('!', '')[:150]
curr_dir = os.path.dirname(os.path.abspath(__file__))
filename = '{filename}.{fmt}'.format(
filename=filename,
fmt=format
)
fullpath = os.path.join(curr_dir, 'mock', filename)
with open(fullpath) as file_data:
return file_data.read()
def find_prompt(self):
return self.get_mock_file('\n', format='txt')
def send_command(self,
command_string,
delay_factor=.1,
max_loops=150,
strip_prompt=True,
strip_command=True):
return self.get_mock_file(command_string)
def send_command_timing(self, command_string, **kvargs):
return self.get_mock_file(command_string)
def receive_data_generator(self):
return ['', ''] # to have an iteration inside private method _netmiko_recv
def send_command_expect(self,
command_string,
expect_string=None,
delay_factor=.2,
max_loops=500,
auto_find_prompt=True,
strip_prompt=True,
strip_command=True):
# for the moment returns the output from send_command only
# this may change in time
return self.send_command(command_string)
class _MockedIOSXRDevice(IOSXR):
"""
Overrides only the very basic methods from the main device driver, that cannot be mocked.
"""
def open(self):
self.device = _MockedNetMikoDevice()
self._cli_prompt = self.device.find_prompt()
self._enter_xml_mode()
def is_alive(self):
return True
class TestIOSXRDevice(unittest.TestCase):
"""
Tests IOS-XR basic functions.
"""
HOSTNAME = 'localhost'
USERNAME = 'vagrant'
PASSWORD = 'vagrant'
PORT = 12205
TIMEOUT = .1 # for tests, smaller values are prefferred
LOCK = False
LOG = sys.stdout
MOCK = True
def __repr__(self):
return 'Connected as {user}@{host}:{port}, timeout is {tout}'.format(
user=self.USERNAME,
host=self.HOSTNAME,
port=self.PORT,
tout=self.TIMEOUT
) if not self.MOCK else 'Simulates device behaviour using mocked data.'
__str__ = __repr__
@classmethod
def setUpClass(cls):
"""
Opens the connection with the IOS-XR device.
"""
if cls.MOCK:
__cls = _MockedIOSXRDevice
else:
__cls = IOSXR
cls.device = __cls(cls.HOSTNAME,
cls.USERNAME,
cls.PASSWORD,
port=cls.PORT,
lock=cls.LOCK,
logfile=cls.LOG,
timeout=cls.TIMEOUT)
cls.device.open()
@classmethod
def tearDownClass(cls):
"""
Closes the connection with the device.
"""
cls.device.close()
def test_mock_lock_connection_open(self):
if self.MOCK:
self.device.lock_on_connect = True
# because there's one single mock file
# and it is already used for the lock test
# will tesst if raises LockError on connect
self.assertRaises(
LockError,
self.device.lock
)
self.device.lock_on_connect = False
# enough to see that will try to lock during connect
def test_mock_close(self):
"""Testing if unlocking when connection is closed"""
if self.MOCK:
self.device.locked = True
self.device.close()
self.assertFalse(self.device.locked,
msg='Cannot unlock the DB.')
def test_execute_rpc_method(self):
"""Testing private method _execute_rpc"""
self.assertIsInstance(
self.device._execute_rpc(''),
ET._Element,
msg='Privat emethod _execute_rpc did not return a valid XML object.'
)
def test__getttr__show_(self):
"""Testing special attribute __getattr___ against valid show command"""
self.assertIsInstance(
self.device.show_ntp_ass(),
str,
'Special attribute __getattr___ did not return a valid string.'
)
def test__getttr__show_args(self):
"""Testing special attribute __getattr___ against valid show command with arguments"""
self.assertIsInstance(
self.device.show_ntp('ass'),
str
)
def test_acquire_xml_agent(self):
"""Testing if able to acquire the XML agent."""
self.device._lock_xml_agent(time.time())
self.assertTrue(self.device._xml_agent_locker.locked())
self.device._unlock_xml_agent()
def test_acquire_locked_agent_raises_timeout_error(self):
"""Testing if trying to acquire the XML agent while locked raises TimeoutError."""
self.device._lock_xml_agent(time.time()) # acquiring
self.assertRaises(
TimeoutError,
self.device._lock_xml_agent, # trying to acquire again
time.time()
)
self.device._unlock_xml_agent() # releasing back
def test_release_xml_agent(self):
"""Testing releasing of XML agent."""
self.device._lock_xml_agent(time.time())
self.assertTrue(self.device._xml_agent_locker.locked())
self.device._unlock_xml_agent()
self.assertFalse(self.device._xml_agent_locker.locked())
def test_in_cli_mode(self):
"""Testing the private method _in_cli_mode."""
self.assertTrue(self.device._in_cli_mode())
def test__getattr_show_config(self):
"""Testing special attribute __getattr___ against valid show config command"""
self.assertIsInstance(
self.device.show_run_ntp(config=True),
str
)
def test__getattr__no_show(self):
""""Test special attribute __getattr__ agains a no-show command"""
raised = False
try:
self.device.configure_exclusive()
except AttributeError:
raised = True
self.assertTrue(raised)
def test_make_rpc_call_returns_XML(self):
"""Test if public method make_rpc_call returns str"""
self.assertIsInstance(
self.device.make_rpc_call(''),
binary_type
)
def test_acquired_xml_agent(self):
"""Testing if raises TimeoutError if the XML agent is alredy acquired and released when exception thrown"""
self.device._lock_xml_agent(time.time()) # acquiring the XML agent
self.assertRaises(
TimeoutError,
self.device.make_rpc_call,
''
)
self.assertFalse(self.device._xml_agent_locker.locked()) # Exception raised => xml agent released
def test_try_to_read_till_timeout(self):
"""Testing if will try to read from the device till time out"""
if self.MOCK:
# hard to reproduce without mock data
# as this event is not deterministic
self.assertRaises(
TimeoutError,
self.device.make_rpc_call,
''
)
def test_multiple_read_attempts_till_timeout(self):
"""Testing if will try to read non-empty replies from the device till time out"""
if self.MOCK:
# hard to reproduce without mock data
# as this event is not deterministic
self.assertRaises(
TimeoutError,
self.device.make_rpc_call,
''
)
def test_iterator_id_raises_IteratorIDError(self):
"""Testing if reply containing the IteratorID attribute raises IteratorIDError"""
self.device.load_candidate_config(config='xml agent tty iteration on size 1')
# minimum iteration size
self.device.commit_config(comment='pyIOSXR-test_xml-agent-iteration-on')
# turning on iteration
# and a very small value
# requesting something that we know for sure will be a big output
self.assertRaises(
IteratorIDError,
self.device.make_rpc_call,
''
)
self.device.rollback()
# going to prev state
def test_channel_acquired_enter_xml_mode(self):
"""Test if not raises ConnectError when the channel is busy with other requests"""
self.device._lock_xml_agent()
self.assertIsNone(self.device._enter_xml_mode())
def test_truncated_response_raises_InvalidXMLResponse(self):
"""Testing if truncated XML reply raises InvalidXMLResponse"""
if self.MOCK:
# hard to reproduce without mock data
# as this event is not deterministic
self.assertRaises(
InvalidXMLResponse,
self.device._execute_rpc,
''
)
def test_iosxr_bug_0x44318c06(self):
"""Tests if IOS-XR bug returns error 0x44318c06 and raise XMLCLIError"""
if self.MOCK:
# hard to reproduce this without mock data
# as this event is not deterministic
self.assertRaises(
XMLCLIError,
self.device._execute_config_show,
'show commit changes diff'
)
def test_empty_reply_raises_TimeoutError(self):
"""Testing if empty reply raises TimeoutError"""
if self.MOCK:
# hard to reproduce this without mock data
# as this event is not deterministic
self.assertRaises(
TimeoutError,
self.device._execute_rpc,
''
)
def test_multiple_requests_raise_0xa3679e00(self):
"""Testing if simultaneuous requests trigger XMLCLIError"""
if self.MOCK:
self.assertRaises(
XMLCLIError,
self.device._execute_rpc,
''
)
else:
# must create a multithreading and send a couple of simultaneous requests to the device
pass
def test_execute_show(self):
"""Testing private method _execute_show"""
self.assertIsInstance(
self.device._execute_show('show ntp ass'),
str
)
def test_execute_invalid_show_raises_InvalidInputError(self):
"""Testing if invalid show command raises InvalidInputError"""
self.assertRaises(
InvalidInputError,
self.device._execute_show,
'sh fake'
)
def test_execute_config_show(self):
"""Testing private method _execute_config_show"""
self.assertIsInstance(
self.device._execute_config_show('show run ntp'),
str
)
def test_execute_invalid_config_show_raises_InvalidInputError(self):
"""Testing if invalid config show command raises InvalidInputError"""
self.assertRaises(
InvalidInputError,
self.device._execute_config_show,
'sh run fake'
)
def test_lock_raises_LockError(self):
"""Tests if DB already locked raises LockError"""
if self.MOCK:
self.assertRaises(
LockError,
self.device.lock
)
self.assertFalse(self.device.locked)
else:
self.device.unlock() # make sure the config is not locked
same_device = IOSXR(self.HOSTNAME,
self.USERNAME,
self.PASSWORD,
port=self.PORT,
lock=self.LOCK,
logfile=self.LOG,
timeout=self.TIMEOUT)
same_device.open()
same_device.lock()
# the other instance locks the config DB
try:
# trying to acquire the config DB
self.device.lock()
except LockError:
self.assertFalse(self.device.locked)
else:
self.assertTrue(self.device.locked)
same_device.close()
def test_unlock(self):
"""Testing unlock feature"""
if self.MOCK:
self.device.lock = True # make sure it is locked
self.device.unlock()
self.assertFalse(self.device.locked)
else:
# make sure this process acquires the config DB
self.device.lock()
try:
self.device.unlock()
except UnlockError:
# still locked
self.assertTrue(self.device.locked)
else:
# not locked anymore
self.assertFalse(self.device.locked)
def _load_dummy_config(self):
"""Helper that loads some dummy data before committing."""
config = '''
ntp peer 172.17.17.1
'''
return self.device.load_candidate_config(config=config)
def test_load_invalid_config_raises_InvalidInputError(self):
"""Testing if loading config with mistakes raises InvalidInputError"""
self.assertRaises(
InvalidInputError,
self.device.load_candidate_config,
config='ntp beer 256.257.258.259'
)
self.device.discard_config()
def test_load_candidate_config_file(self):
"""Testing loading candidate config from file"""
self.assertIsNone(
self.device.load_candidate_config(
filename=os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'mock',
'good.cfg'
)
)
)
def test_load_invalid_candidate_config_file_raises_InvalidInputError(self):
"""Testing if loading invalid config from a file raises InvalidInputError"""
self.assertRaises(
InvalidInputError,
self.device.load_candidate_config,
filename=os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'mock',
'bad.cfg'
)
)
def test_load_config(self):
"""Testing if able to load candidate config, then check commit diff and discard changes"""
self._load_dummy_config()
self.assertIsInstance(
self.device.get_candidate_config(),
str,
msg='Unable to retrieve the candidate config'
)
self.assertIsInstance(
self.device.get_candidate_config(merge=True),
str,
msg='Unable to retrieve merge candidate config'
)
self.assertIsInstance(
self.device.get_candidate_config(formal=True),
str,
msg='Unable to retrieve formal candidate config'
)
compare_result = self.device.compare_config()
self.assertIsInstance(
compare_result,
str,
msg='Unable to compare running and candidate config'
)
# test if the result is string
self.assertGreater(
len(compare_result),
0,
msg='No config changes applied.'
)
# test if len > 0
# discarding config
self.device.discard_config()
if not self.MOCK:
# will get the same mock file as above
self.assertEqual(
len(self.device.compare_config()),
0,
msg='Unable to discard changes'
)
def test_commit_config(self):
"""Testing commit config"""
self._load_dummy_config()
self.assertIsNone(self.device.commit_config())
self.device.rollback()
def test_commit_config_message(self):
"""Testing commit config with comment message"""
self._load_dummy_config()
self.assertIsNone(self.device.commit_config(comment="good"))
self.device.rollback()
def test_commit_config_label(self):
"""Testing commit config with label"""
self._load_dummy_config()
self.assertIsNone(self.device.commit_config(label="test"))
self.device.rollback()
def test_commit_config_confirmed(self):
"""Testing commit confirmed"""
self._load_dummy_config()
self.assertIsNone(self.device.commit_config(confirmed=60))
self.device.rollback()
def test_commit_config_confirmed_raise_InvalidInputError(self):
"""Testing if incorrect value for confirm time raises InvalidInputError"""
self.assertRaises(
InvalidInputError,
self.device.commit_config,
confirmed=1
)
def test_commit_empty_buffer_raises(self):
"""Testing if trying to commit empty changes raises CommitError"""
self.assertRaises(
CommitError,
self.device.commit_config,
comment="empty"
)
def test_commit_after_other_session_commit(self):
"""Testing if trying to commit after another process commited does not raise CommitError"""
if self.MOCK:
# mock data contains the error message we are looking for
self.assertIsNone(self.device.commit_config(comment="parallel"))
else:
# to test this will neet to apply changes to the same device
# through a different SSH session
same_device = IOSXR(self.HOSTNAME,
self.USERNAME,
self.PASSWORD,
port=self.PORT,
lock=self.LOCK,
logfile=self.LOG,
timeout=self.TIMEOUT)
same_device.open()
# loading something
same_device.load_candidate_config(
config='interface MgmtEth0/RP0/CPU0/0 description testing parallel commits'
)
# committing
same_device.commit_config(comment='pyIOSXR-test_parallel_commits')
# trying to load something from the test instance
self.device.load_candidate_config(config='interface MgmtEth0/RP0/CPU0/0 description this wont work')
# and will fail because of the commit above
self.assertIsNone(self.device.commit_config(comment="parallel"))
# let's rollback the committed changes
same_device.rollback()
# and close the auxiliary connection
same_device.close()
# because this error was raised
self.device.close()
self.device.open()
def _prefetch_running_config_and_append(self):
"""Helper method to be used in the config-replace tests below"""
running_config = ''.join(self.device.show_run().splitlines(1)[3:])
self.device.load_candidate_config(config=running_config)
self.device.load_candidate_config(config='ntp server 8.8.8.8')
def test_compare_replace_config(self):
"""Testing compare replace config"""
self._prefetch_running_config_and_append()
self.assertIsInstance(self.device.compare_replace_config(), str)
def test_commit_replace_config(self):
"""Testing commit replace config"""
self._prefetch_running_config_and_append()
self.assertIsNone(self.device.commit_replace_config())
def test_commit_replace_config_message(self):
"""Testing commit replace config with comment message"""
self._prefetch_running_config_and_append()
self.assertIsNone(self.device.commit_replace_config(comment="good"))
def test_commit_replace_config_label(self):
"""Testing commit replace config with label"""
self._prefetch_running_config_and_append()
self.assertIsNone(self.device.commit_replace_config(label="test"))
def test_commit_replace_config_confirmed(self):
"""Testing commit replace confirmed"""
self._prefetch_running_config_and_append()
self.assertIsNone(self.device.commit_replace_config(confirmed=60))
def test_commit_replace_config_confirmed_raise_InvalidInputError(self):
"""Testing if incorrect value for confirmed replace commit time raises InvalidInputError"""
self.assertRaises(
InvalidInputError,
self.device.commit_replace_config,
confirmed=500
)
if __name__ == '__main__':
unittest.main()
pyIOSXR-0.52/MANIFEST.in 0000644 0001750 0001750 00000000031 13135644353 015316 0 ustar travis travis 0000000 0000000 include requirements.txt
pyIOSXR-0.52/requirements.txt 0000644 0001750 0001750 00000000033 13135644353 017046 0 ustar travis travis 0000000 0000000 netmiko>=1.4.1
lxml>=3.2.4
pyIOSXR-0.52/setup.cfg 0000644 0001750 0001750 00000000117 13135644413 015403 0 ustar travis travis 0000000 0000000 [metadata]
description-file = README.md
[egg_info]
tag_build =
tag_date = 0
pyIOSXR-0.52/setup.py 0000644 0001750 0001750 00000003262 13135644353 015303 0 ustar travis travis 0000000 0000000 #!/usr/bin/env python
# coding=utf-8
"""A module to interact with Cisco devices running IOS-XR."""
# Copyright 2015 Netflix. All rights reserved.
# Copyright 2016 BigWaveIT. All rights reserved.
#
# The contents of this file are 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 setuptools import setup, find_packages
from pip.req import parse_requirements
import uuid
# parse_requirements() returns generator of pip.req.InstallRequirement objects
install_reqs = parse_requirements('requirements.txt', session=uuid.uuid1())
# reqs is a list of requirement
# e.g. ['django==1.5.1', 'mezzanine==1.4.6']
reqs = [str(ir.req) for ir in install_reqs]
version = '0.52'
setup(
name='pyIOSXR',
version=version,
py_modules=['pyIOSXR'],
packages=find_packages(),
install_requires=reqs,
include_package_data=True,
description='Python API to interact with network devices running IOS-XR',
author='Elisa Jasinska, Mircea Ulinic',
author_email='elisa@bigwaveit.org, mircea@cloudflare.com',
url='https://github.com/fooelisa/pyiosxr/',
download_url='https://github.com/fooelisa/pyiosxr/tarball/%s' % version,
keywords=['IOS-XR', 'IOSXR', 'Cisco', 'networking'],
classifiers=[],
)
pyIOSXR-0.52/PKG-INFO 0000644 0001750 0001750 00000000644 13135644413 014664 0 ustar travis travis 0000000 0000000 Metadata-Version: 1.1
Name: pyIOSXR
Version: 0.52
Summary: Python API to interact with network devices running IOS-XR
Home-page: https://github.com/fooelisa/pyiosxr/
Author: Elisa Jasinska, Mircea Ulinic
Author-email: elisa@bigwaveit.org, mircea@cloudflare.com
License: UNKNOWN
Download-URL: https://github.com/fooelisa/pyiosxr/tarball/0.52
Description: UNKNOWN
Keywords: IOS-XR,IOSXR,Cisco,networking
Platform: UNKNOWN