pyIOSXR-0.52/0000755000175000017500000000000013135644413013563 5ustar travistravis00000000000000pyIOSXR-0.52/pyIOSXR/0000755000175000017500000000000013135644413015040 5ustar travistravis00000000000000pyIOSXR-0.52/pyIOSXR/__init__.py0000644000175000017500000000144113135644353017154 0ustar travistravis00000000000000#!/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.py0000644000175000017500000000515613135644353017605 0ustar travistravis00000000000000#!/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.py0000644000175000017500000006364713135644353016601 0ustar travistravis00000000000000# -*- 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) # '): 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 = '=1.4.1 lxml>=3.2.4 pyIOSXR-0.52/pyIOSXR.egg-info/top_level.txt0000644000175000017500000000001013135644413021253 0ustar travistravis00000000000000pyIOSXR pyIOSXR-0.52/test/0000755000175000017500000000000013135644413014542 5ustar travistravis00000000000000pyIOSXR-0.52/test/test.py0000755000175000017500000005456313135644353016116 0ustar travistravis00000000000000#!/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.in0000644000175000017500000000003113135644353015316 0ustar travistravis00000000000000include requirements.txt pyIOSXR-0.52/requirements.txt0000644000175000017500000000003313135644353017046 0ustar travistravis00000000000000netmiko>=1.4.1 lxml>=3.2.4 pyIOSXR-0.52/setup.cfg0000644000175000017500000000011713135644413015403 0ustar travistravis00000000000000[metadata] description-file = README.md [egg_info] tag_build = tag_date = 0 pyIOSXR-0.52/setup.py0000644000175000017500000000326213135644353015303 0ustar travistravis00000000000000#!/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-INFO0000644000175000017500000000064413135644413014664 0ustar travistravis00000000000000Metadata-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