├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── commit.yaml │ └── publish.yaml ├── .gitignore ├── .pylintrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── _config.yml ├── comparison_tests ├── hosts.py ├── test_netmiko.py └── test_ssh2net.py ├── docker-compose.yaml ├── docs └── ssh2net │ ├── base.html │ ├── channel.html │ ├── community │ └── index.html │ ├── core │ ├── arista_eos │ │ ├── driver.html │ │ └── index.html │ ├── cisco_iosxe │ │ ├── driver.html │ │ └── index.html │ ├── cisco_iosxr │ │ ├── driver.html │ │ └── index.html │ ├── cisco_nxos │ │ ├── driver.html │ │ └── index.html │ ├── driver.html │ ├── index.html │ └── juniper_junos │ │ ├── driver.html │ │ ├── helper.html │ │ └── index.html │ ├── decorators.html │ ├── exceptions.html │ ├── helper.html │ ├── index.html │ ├── netmiko_compatibility.html │ ├── session.html │ ├── session_miko.html │ ├── session_ssh2.html │ └── ssh_config.html ├── examples ├── basic_usage │ ├── basic_usage_ssh2net_connecthandler_style.py │ ├── basic_usage_ssh2net_driver_style.py │ └── basic_usage_ssh2net_native_style.py ├── logging │ └── session_and_channel_log_diff_files.py ├── ssh_keys │ └── ssh_key_basic.py └── textfsm │ └── textfsm_parse_output.py ├── pyproject.toml ├── requirements-dev.txt ├── requirements-paramiko.txt ├── requirements-textfsm.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── ssh2net ├── __init__.py ├── base.py ├── channel.py ├── community │ └── __init__.py ├── core │ ├── __init__.py │ ├── arista_eos │ │ ├── __init__.py │ │ └── driver.py │ ├── cisco_iosxe │ │ ├── __init__.py │ │ └── driver.py │ ├── cisco_iosxr │ │ ├── __init__.py │ │ └── driver.py │ ├── cisco_nxos │ │ ├── __init__.py │ │ └── driver.py │ ├── driver.py │ └── juniper_junos │ │ ├── __init__.py │ │ ├── driver.py │ │ └── helper.py ├── decorators.py ├── exceptions.py ├── helper.py ├── netmiko_compatibility.py ├── session.py ├── session_miko.py ├── session_ssh2.py └── ssh_config.py ├── tests ├── __init__.py ├── functional │ ├── __init__.py │ ├── arista_eos │ │ ├── expected_output │ │ │ ├── show_run │ │ │ └── show_run_no_strip │ │ ├── ext_test_funcs.py │ │ └── test_eos.py │ ├── base_functional_tests.py │ ├── cisco_iosxe │ │ ├── expected_output │ │ │ ├── interactive │ │ │ ├── show_run │ │ │ ├── show_run_execute │ │ │ └── show_run_no_strip │ │ ├── ext_test_funcs.py │ │ └── test_iosxe.py │ ├── cisco_iosxr │ │ ├── expected_output │ │ │ ├── interactive │ │ │ ├── show_run │ │ │ ├── show_run_execute │ │ │ └── show_run_no_strip │ │ ├── ext_test_funcs.py │ │ └── test_iosxr.py │ ├── cisco_nxos │ │ ├── expected_output │ │ │ ├── interactive │ │ │ ├── show_run │ │ │ ├── show_run_execute │ │ │ └── show_run_no_strip │ │ ├── ext_test_funcs.py │ │ └── test_nxos.py │ ├── comparison_tests │ │ └── test_comparison_tests.py │ ├── examples │ │ └── README.md │ └── juniper_junos │ │ ├── expected_output │ │ ├── interactive │ │ ├── show_run │ │ └── show_run_no_strip │ │ ├── ext_test_funcs.py │ │ └── test_junos.py └── unit │ ├── __init__.py │ ├── _ssh_config │ ├── drivers │ ├── __init__.py │ ├── base_driver_unit_tests.py │ └── core │ │ ├── __init__.py │ │ ├── test_core_arista_eos_driver.py │ │ ├── test_core_cisco_iosxe_driver.py │ │ ├── test_core_cisco_iosxr_driver.py │ │ ├── test_core_cisco_nxos_driver.py │ │ └── test_core_juniper_junos_driver.py │ ├── ext_test_funcs.py │ ├── test_base.py │ ├── test_channel.py │ ├── test_core_driver.py │ ├── test_decorators.py │ ├── test_helper.py │ ├── test_netmiko_compatibility.py │ ├── test_session.py │ └── test_ssh_config.py └── tox.ini /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Your script 16 | 2. What you're connecting to (vendor, platform, version) 17 | 3. Anything else relevant 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Stack Trace** 23 | Copy of your stack trace here, please format it properly using triple back ticks (top left key on US keyboards!) 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **OS (please complete the following information):** 29 | - OS: [e.g. Ubuntu, MacOS, etc. - Note this is *not* tested on Windows and likely will not be supported] 30 | - ssh2net version 31 | - ssh2python version 32 | - python version 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/workflows/commit.yaml: -------------------------------------------------------------------------------- 1 | name: linting and unit tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | max-parallel: 6 10 | matrix: 11 | os: [ubuntu-latest, macos-latest] 12 | python-version: [3.6, 3.7, 3.8] 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: set up python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: setup test env 20 | run: | 21 | python -m pip install --upgrade pip 22 | python -m pip install setuptools 23 | python -m pip install tox 24 | - name: run tox 25 | run: python -m tox --skip-missing-interpreters=true 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: re-test and publish to pypi 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | max-parallel: 6 12 | matrix: 13 | os: [ubuntu-latest, macos-latest] 14 | python-version: [3.6, 3.7, 3.8] 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: set up python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: setup test env 22 | run: | 23 | python -m pip install --upgrade pip 24 | python -m pip install setuptools 25 | python -m pip install tox 26 | - name: run tox 27 | run: python -m tox --skip-missing-interpreters=true 28 | 29 | deploy: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v1 33 | - name: set up python 3.7 34 | uses: actions/setup-python@v1 35 | with: 36 | python-version: 3.7 37 | - name: setup publish env 38 | run: | 39 | python -m pip install --upgrade pip 40 | python -m pip install setuptools 41 | python -m pip install wheel 42 | python -m pip install twine 43 | - name: build and publish 44 | env: 45 | TWINE_USERNAME: ${{ secrets.PYPI_USER }} 46 | TWINE_PASSWORD: ${{ secrets.PYPI_PASS }} 47 | run: | 48 | python setup.py sdist bdist_wheel 49 | python -m twine upload dist/* 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Environments 54 | .env 55 | .venv 56 | env/ 57 | venv/ 58 | ENV/ 59 | env.bak/ 60 | venv.bak/ 61 | 62 | # pycharm 63 | .idea 64 | 65 | # mypy 66 | .mypy_cache/ 67 | .dmypy.json 68 | dmypy.json 69 | 70 | # swap files 71 | *.swp 72 | 73 | # macos stuff 74 | .DS_Store 75 | */.DS_Store 76 | 77 | # private dir for notes and such 78 | private/ 79 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Be excellent to each other! 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This was created as a learning project, so contributions are not expected, but are very welcome! Feel free to open PRs or Issues as needed. Any contributions would need to at a minimum successfully complete a tox run since there is no CI at this point. 4 | 5 | Between tox and the Makefile, I hope that testing should be pretty easy, with a big caveat that you will need to get vrnetlab setup and working for functional testing to occur in a reasonable and repeatable fashion. 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Carl Montanari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_COMPOSE_FILE=docker-compose.yaml 2 | DOCKER_COMPOSE=docker-compose -f ${DOCKER_COMPOSE_FILE} 3 | 4 | start_dev_env: 5 | ${DOCKER_COMPOSE} \ 6 | up -d \ 7 | iosxe \ 8 | nxos \ 9 | junos \ 10 | iosxr 11 | 12 | start_dev_env_iosxe: 13 | ${DOCKER_COMPOSE} \ 14 | up -d \ 15 | iosxe 16 | 17 | start_dev_env_nxos: 18 | ${DOCKER_COMPOSE} \ 19 | up -d \ 20 | nxos 21 | 22 | start_dev_env_iosxr: 23 | ${DOCKER_COMPOSE} \ 24 | up -d \ 25 | iosxr 26 | 27 | start_dev_env_junos: 28 | ${DOCKER_COMPOSE} \ 29 | up -d \ 30 | junos 31 | 32 | start_dev_env_eos: 33 | ${DOCKER_COMPOSE} \ 34 | up -d \ 35 | eos 36 | 37 | stop_dev_env: 38 | ${DOCKER_COMPOSE} \ 39 | down 40 | 41 | # run with sudo! 42 | add_delay_dev_env: 43 | tc qdisc del dev br_ssh2net root; \ 44 | tc qdisc add dev br_ssh2net root handle 1: prio; \ 45 | tc qdisc add dev br_ssh2net parent 1:3 handle 30: tbf rate 20kbit buffer 1600 limit 3000; \ 46 | tc qdisc add dev br_ssh2net parent 30:1 handle 31: netem delay 1000ms 10ms distribution normal loss 10%; \ 47 | tc filter add dev br_ssh2net protocol ip parent 1:0 prio 3 u32 match ip dst 172.18.0.0/26 flowid 1:3 48 | 49 | # run with sudo! 50 | remove_delay_dev_env: 51 | tc qdisc del dev br_ssh2net root 52 | 53 | test_unit: 54 | python -m pytest \ 55 | --cov=ssh2net \ 56 | --cov-report html \ 57 | --cov-report term \ 58 | tests/unit/. 59 | 60 | test_functional: 61 | python -m pytest \ 62 | --cov=ssh2net \ 63 | --cov-report html \ 64 | --cov-report term \ 65 | tests/functional/. \ 66 | --ignore tests/functional/cisco_iosxr \ 67 | --ignore tests/functional/arista_eos \ 68 | --ignore tests/functional/comparison_tests 69 | 70 | test_all: 71 | python -m pytest \ 72 | --cov=ssh2net \ 73 | --cov-report html \ 74 | --cov-report term \ 75 | tests/. \ 76 | --ignore tests/functional/cisco_iosxr \ 77 | --ignore tests/functional/arista_eos \ 78 | --ignore tests/functional/comparison_tests 79 | 80 | test_iosxe: 81 | python -m pytest \ 82 | --cov=ssh2net \ 83 | --cov-report html \ 84 | --cov-report term \ 85 | tests/unit \ 86 | tests/functional/cisco_iosxe 87 | 88 | test_nxos: 89 | python -m pytest \ 90 | --cov=ssh2net \ 91 | --cov-report html \ 92 | --cov-report term \ 93 | tests/unit \ 94 | tests/functional/cisco_nxos 95 | 96 | test_iosxr: 97 | python -m pytest \ 98 | --cov=ssh2net \ 99 | --cov-report html \ 100 | --cov-report term \ 101 | tests/unit \ 102 | tests/functional/cisco_iosxr 103 | 104 | test_junos: 105 | python -m pytest \ 106 | --cov=ssh2net \ 107 | --cov-report html \ 108 | --cov-report term \ 109 | tests/unit \ 110 | tests/functional/juniper_junos 111 | 112 | test_eos: 113 | python -m pytest \ 114 | --cov=ssh2net \ 115 | --cov-report html \ 116 | --cov-report term \ 117 | tests/unit \ 118 | tests/functional/arista_eos 119 | 120 | .PHONY: docs 121 | docs: 122 | python -m pdoc \ 123 | --html \ 124 | --output-dir docs \ 125 | ssh2net \ 126 | --force 127 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-midnight -------------------------------------------------------------------------------- /comparison_tests/hosts.py: -------------------------------------------------------------------------------- 1 | IOSXE_TEST = { 2 | "host": "172.18.0.11", 3 | "username": "vrnetlab", 4 | "password": "VR-netlab9", 5 | "device_type": "cisco_xe", 6 | "test_commands": ["show run", "show version"], 7 | } 8 | 9 | NXOS_TEST = { 10 | "host": "172.18.0.12", 11 | "username": "vrnetlab", 12 | "password": "VR-netlab9", 13 | "device_type": "cisco_nxos", 14 | "test_commands": ["show run", "show version"], 15 | } 16 | 17 | IOSXR_TEST = { 18 | "host": "172.18.0.13", 19 | "username": "vrnetlab", 20 | "password": "VR-netlab9", 21 | "device_type": "cisco_iosxr", 22 | "test_commands": ["show run", "show version"], 23 | } 24 | 25 | # need to get arista_eos image in vrnetlab for testing + fix libssh2 keyboard interactive auth issue 26 | # EOS_TEST = { 27 | # "host": "172.18.0.14", 28 | # "username": "vrnetlab", 29 | # "password": "VR-netlab9", 30 | # "device_type": "arista_eos", 31 | # "test_commands": ["show run", "show version"], 32 | # } 33 | 34 | JUNOS_TEST = { 35 | "host": "172.18.0.15", 36 | "username": "vrnetlab", 37 | "password": "VR-netlab9", 38 | "device_type": "juniper_junos", 39 | "test_commands": ["show configuration", "show version"], 40 | } 41 | -------------------------------------------------------------------------------- /comparison_tests/test_netmiko.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from datetime import datetime 3 | 4 | from netmiko import ConnectHandler 5 | 6 | import hosts 7 | 8 | IOSXE_TEST = hosts.IOSXE_TEST 9 | NXOS_TEST = hosts.NXOS_TEST 10 | test_hosts = [IOSXE_TEST, NXOS_TEST] 11 | 12 | 13 | def main(): 14 | for host in test_hosts: 15 | commands = host.pop("test_commands") 16 | conn = ConnectHandler(**host) 17 | for command in commands: 18 | print(f"Sending command: '{command}'") 19 | print("*" * 80) 20 | command_start_time = datetime.now() 21 | r = conn.send_command(command) 22 | command_end_time = datetime.now() 23 | print(r) 24 | print("*" * 80) 25 | print( 26 | f"Sending command: '{command}' took {command_end_time - command_start_time} seconds!" 27 | ) 28 | print("*" * 80) 29 | print() 30 | 31 | 32 | if __name__ == "__main__": 33 | start_time = datetime.now() 34 | main() 35 | end_time = datetime.now() 36 | print(f"Full test took {end_time - start_time} seconds!") 37 | -------------------------------------------------------------------------------- /comparison_tests/test_ssh2net.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from datetime import datetime 3 | 4 | from ssh2net import SSH2Net 5 | 6 | import hosts 7 | 8 | IOSXE_TEST = hosts.IOSXE_TEST 9 | NXOS_TEST = hosts.NXOS_TEST 10 | test_hosts = [IOSXE_TEST, NXOS_TEST] 11 | 12 | 13 | def main(): 14 | for host in test_hosts: 15 | del host["device_type"] 16 | commands = host.pop("test_commands") 17 | host["setup_host"] = host.pop("host") 18 | host["auth_user"] = host.pop("username") 19 | host["auth_password"] = host.pop("password") 20 | conn = SSH2Net(**host) 21 | conn.open_shell() 22 | for command in commands: 23 | print(f"Sending command: '{command}'") 24 | print("*" * 80) 25 | command_start_time = datetime.now() 26 | r = conn.send_inputs(command) 27 | command_end_time = datetime.now() 28 | print(r[0]) 29 | print("*" * 80) 30 | print( 31 | f"Sending command: '{command}' took {command_end_time - command_start_time} seconds!" 32 | ) 33 | print("*" * 80) 34 | print() 35 | 36 | 37 | if __name__ == "__main__": 38 | start_time = datetime.now() 39 | main() 40 | end_time = datetime.now() 41 | print(f"Full test took {end_time - start_time} seconds!") 42 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3.3" 3 | 4 | services: 5 | iosxe: 6 | hostname: cisco_iosxe 7 | privileged: true 8 | image: ssh2netiosxe 9 | networks: 10 | net1: 11 | ipv4_address: 172.18.0.11 12 | 13 | nxos: 14 | hostname: cisco_nxos 15 | privileged: true 16 | image: ssh2netnxos 17 | networks: 18 | net1: 19 | ipv4_address: 172.18.0.12 20 | 21 | iosxr: 22 | hostname: cisco_iosxr 23 | privileged: true 24 | image: ssh2netiosxr 25 | networks: 26 | net1: 27 | ipv4_address: 172.18.0.13 28 | 29 | eos: 30 | hostname: arista_eos 31 | privileged: true 32 | image: ssh2neteos 33 | networks: 34 | net1: 35 | ipv4_address: 172.18.0.14 36 | 37 | junos: 38 | hostname: juniper_junos 39 | privileged: true 40 | image: ssh2netjunos 41 | networks: 42 | net1: 43 | ipv4_address: 172.18.0.15 44 | 45 | networks: 46 | net1: 47 | driver: bridge 48 | ipam: 49 | config: 50 | - subnet: 172.18.0.0/26 51 | driver_opts: 52 | com.docker.network.bridge.name: br_ssh2net 53 | 54 | -------------------------------------------------------------------------------- /docs/ssh2net/community/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ssh2net.community API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |

Module ssh2net.community

21 |
22 |
23 |

ssh2net community platform drivers

24 |
25 | 26 | Expand source code 27 | 28 |
"""ssh2net community platform drivers"""
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | 53 |
54 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /docs/ssh2net/core/arista_eos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ssh2net.core.arista_eos API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |

Module ssh2net.core.arista_eos

21 |
22 |
23 |

ssh2net arista eos driver

24 |
25 | 26 | Expand source code 27 | 28 |
"""ssh2net arista eos driver"""
29 |
30 |
31 |
32 |

Sub-modules

33 |
34 |
ssh2net.core.arista_eos.driver
35 |
36 |

ssh2net.core.arista_eos.driver

37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 65 |
66 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/ssh2net/core/cisco_iosxe/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ssh2net.core.cisco_iosxe API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |

Module ssh2net.core.cisco_iosxe

21 |
22 |
23 |

ssh2net cisco iosxe driver

24 |
25 | 26 | Expand source code 27 | 28 |
"""ssh2net cisco iosxe driver"""
29 |
30 |
31 |
32 |

Sub-modules

33 |
34 |
ssh2net.core.cisco_iosxe.driver
35 |
36 |

ssh2net.core.cisco_iosxe.driver

37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 65 |
66 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/ssh2net/core/cisco_iosxr/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ssh2net.core.cisco_iosxr API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |

Module ssh2net.core.cisco_iosxr

21 |
22 |
23 |

ssh2net cisco iosxr driver

24 |
25 | 26 | Expand source code 27 | 28 |
"""ssh2net cisco iosxr driver"""
29 |
30 |
31 |
32 |

Sub-modules

33 |
34 |
ssh2net.core.cisco_iosxr.driver
35 |
36 |

ssh2net.core.cisco_iosxr.driver

37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 65 |
66 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/ssh2net/core/cisco_nxos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ssh2net.core.cisco_nxos API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |

Module ssh2net.core.cisco_nxos

21 |
22 |
23 |

ssh2net cisco nxos driver

24 |
25 | 26 | Expand source code 27 | 28 |
"""ssh2net cisco nxos driver"""
29 |
30 |
31 |
32 |

Sub-modules

33 |
34 |
ssh2net.core.cisco_nxos.driver
35 |
36 |

ssh2net.core.cisco_nxos.driver

37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 65 |
66 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/ssh2net/core/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ssh2net.core API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |

Module ssh2net.core

21 |
22 |
23 |

ssh2net core platform drivers

24 |
25 | 26 | Expand source code 27 | 28 |
"""ssh2net core platform drivers"""
29 |
30 |
31 |
32 |

Sub-modules

33 |
34 |
ssh2net.core.arista_eos
35 |
36 |

ssh2net arista eos driver

37 |
38 |
ssh2net.core.cisco_iosxe
39 |
40 |

ssh2net cisco iosxe driver

41 |
42 |
ssh2net.core.cisco_iosxr
43 |
44 |

ssh2net cisco iosxr driver

45 |
46 |
ssh2net.core.cisco_nxos
47 |
48 |

ssh2net cisco nxos driver

49 |
50 |
ssh2net.core.driver
51 |
52 |

ssh2net.core.driver

53 |
54 |
ssh2net.core.juniper_junos
55 |
56 |

ssh2net juniper junos driver

57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | 90 |
91 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /docs/ssh2net/core/juniper_junos/helper.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ssh2net.core.juniper_junos.helper API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |

Module ssh2net.core.juniper_junos.helper

21 |
22 |
23 |

ssh2net.core.juniper_junos.helper

24 |
25 | 26 | Expand source code 27 | 28 |
"""ssh2net.core.juniper_junos.helper"""
 29 | 
 30 | 
 31 | def disable_paging(cls):
 32 |     """
 33 |     Disable paging and set screen width for Junos
 34 | 
 35 |     Args:
 36 |         cls: SSH2Net connection object
 37 | 
 38 |     Returns:
 39 |         N/A  # noqa
 40 | 
 41 |     Raises:
 42 |         N/A  # noqa
 43 |     """
 44 |     cls.send_inputs("set cli screen-length 0")
 45 |     cls.send_inputs("set cli screen-width 511")
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |

Functions

54 |
55 |
56 | def disable_paging(cls) 57 |
58 |
59 |

Disable paging and set screen width for Junos

60 |

Args

61 |
62 |
cls
63 |
SSH2Net connection object
64 |
65 |

Returns

66 |
67 |
N/A 68 | # noqa
69 |
 
70 |
71 |

Raises

72 |
73 |
N/A 74 | # noqa
75 |
 
76 |
77 |
78 | 79 | Expand source code 80 | 81 |
def disable_paging(cls):
 82 |     """
 83 |     Disable paging and set screen width for Junos
 84 | 
 85 |     Args:
 86 |         cls: SSH2Net connection object
 87 | 
 88 |     Returns:
 89 |         N/A  # noqa
 90 | 
 91 |     Raises:
 92 |         N/A  # noqa
 93 |     """
 94 |     cls.send_inputs("set cli screen-length 0")
 95 |     cls.send_inputs("set cli screen-width 511")
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | 121 |
122 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /docs/ssh2net/core/juniper_junos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ssh2net.core.juniper_junos API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |

Module ssh2net.core.juniper_junos

21 |
22 |
23 |

ssh2net juniper junos driver

24 |
25 | 26 | Expand source code 27 | 28 |
"""ssh2net juniper junos driver"""
29 |
30 |
31 |
32 |

Sub-modules

33 |
34 |
ssh2net.core.juniper_junos.driver
35 |
36 |

ssh2net.core.juniper_junos.driver

37 |
38 |
ssh2net.core.juniper_junos.helper
39 |
40 |

ssh2net.core.juniper_junos.helper

41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 70 |
71 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /examples/basic_usage/basic_usage_ssh2net_connecthandler_style.py: -------------------------------------------------------------------------------- 1 | from ssh2net import ConnectHandler 2 | 3 | my_device = { 4 | "device_type": "cisco_xe", 5 | "host": "172.18.0.11", 6 | "username": "vrnetlab", 7 | "password": "VR-netlab9", 8 | } 9 | 10 | driver = ConnectHandler(**my_device) 11 | output = driver.send_command("show version") 12 | # send_inputs returns a list of results; print the zeroith result 13 | print(output[0]) 14 | driver.send_config_set(["interface loopback123", "description ssh2net was here"]) 15 | output = driver.send_command("show run int loopback123") 16 | print(output[0]) 17 | driver.send_config_set("no interface loopback123") 18 | -------------------------------------------------------------------------------- /examples/basic_usage/basic_usage_ssh2net_driver_style.py: -------------------------------------------------------------------------------- 1 | from ssh2net.core.cisco_iosxe.driver import IOSXEDriver 2 | 3 | my_device = {"setup_host": "172.18.0.11", "auth_user": "vrnetlab", "auth_password": "VR-netlab9"} 4 | 5 | iosxe_driver = IOSXEDriver 6 | 7 | with IOSXEDriver(**my_device) as conn: 8 | output = conn.send_command("show version") 9 | # send_inputs returns a list of results; print the zeroith result 10 | print(output[0]) 11 | conn.send_config_set(["interface loopback123", "description ssh2net was here"]) 12 | output = conn.send_command("show run int loopback123") 13 | print(output[0]) 14 | conn.send_config_set("no interface loopback123") 15 | -------------------------------------------------------------------------------- /examples/basic_usage/basic_usage_ssh2net_native_style.py: -------------------------------------------------------------------------------- 1 | from ssh2net import SSH2Net 2 | 3 | # Example assumes IOSXE, but should work on most platform where the command syntax below is valid! 4 | 5 | my_device = {"setup_host": "172.18.0.11", "auth_user": "vrnetlab", "auth_password": "VR-netlab9"} 6 | 7 | # Example with context manager: 8 | with SSH2Net(**my_device) as conn: 9 | output = conn.send_inputs("show version") 10 | # send_inputs returns a list of results; print the zeroith result 11 | print(output[0]) 12 | # native style doesn't handle privilege escalation/deescalation for you 13 | conn.send_inputs(["conf t", "interface loopback123", "description ssh2net was here", "end"]) 14 | output = conn.send_inputs("show run int loopback123") 15 | print(output[0]) 16 | conn.send_inputs(["conf t", "no interface loopback123", "end"]) 17 | 18 | # Example without context manager; note you need to open the shell without the context manager! 19 | conn = SSH2Net(**my_device) 20 | conn.open_shell() 21 | output = conn.send_inputs("show version") 22 | print(output[0]) 23 | conn.send_inputs(["conf t", "interface loopback123", "description ssh2net was here", "end"]) 24 | output = conn.send_inputs("show run int loopback123") 25 | print(output[0]) 26 | conn.send_inputs(["conf t", "no interface loopback123", "end"]) 27 | -------------------------------------------------------------------------------- /examples/logging/session_and_channel_log_diff_files.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import ssh2net 3 | 4 | 5 | # Set desired log format 6 | log_format = "%(asctime)s [%(levelname)s] in %(pathname)s:%(lineno)d \n\t %(message)s" 7 | 8 | # Setup session logging; session logging is connection stuff only -- i.e. socket/session/channel 9 | session_log_file = "session.log" 10 | # Set the "ssh2net_session" logger 11 | session_logger = logging.getLogger("ssh2net_session") 12 | # Set log level to level of your choice 13 | session_logger.setLevel(logging.DEBUG) 14 | session_logger_file_handler = logging.FileHandler(session_log_file) 15 | # Assign formatter to log handler 16 | session_logger_file_handler.setFormatter(logging.Formatter(log_format)) 17 | # Add log handler to the session_logger 18 | session_logger.addHandler(session_logger_file_handler) 19 | # Do not propagate logs to stdout 20 | session_logger.propagate = False 21 | 22 | # Repeat similar steps as above for channel logger; channel logger captures reads/writes 23 | # ssh2net_channel logger removes duplicate entries from the log to avoid logs being massive 24 | channel_log_file = "channel.log" 25 | channel_logger = logging.getLogger("ssh2net_channel") 26 | channel_logger.setLevel(logging.DEBUG) 27 | channel_logger_file_handler = logging.FileHandler(channel_log_file) 28 | channel_logger_file_handler.setFormatter(logging.Formatter(log_format)) 29 | channel_logger.addHandler(channel_logger_file_handler) 30 | channel_logger.propagate = False 31 | 32 | my_device = {"setup_host": "172.18.0.11", "auth_user": "vrnetlab", "auth_password": "VR-netlab9"} 33 | with ssh2net.SSH2Net(**my_device) as conn: 34 | show_run = conn.send_inputs("show run") 35 | 36 | print(show_run[0]) 37 | -------------------------------------------------------------------------------- /examples/ssh_keys/ssh_key_basic.py: -------------------------------------------------------------------------------- 1 | from ssh2net.core.cisco_iosxe.driver import IOSXEDriver 2 | 3 | my_device = { 4 | "setup_host": "IP/NAME", 5 | "auth_user": "USERNAME", 6 | "auth_public_key": "/PATH/TO/KEY/TO/USE", 7 | } 8 | 9 | 10 | with IOSXEDriver(**my_device) as conn: 11 | output = conn.send_command("show version") 12 | print(output[0]) 13 | conn.send_config_set(["interface loopback123", "description ssh2net was here"]) 14 | output = conn.send_command("show run int loopback123") 15 | print(output[0]) 16 | conn.send_config_set("no interface loopback123") 17 | -------------------------------------------------------------------------------- /examples/textfsm/textfsm_parse_output.py: -------------------------------------------------------------------------------- 1 | from ssh2net.core.cisco_iosxe.driver import IOSXEDriver 2 | 3 | my_device = {"setup_host": "172.18.0.11", "auth_user": "vrnetlab", "auth_password": "VR-netlab9"} 4 | 5 | 6 | with IOSXEDriver(**my_device) as conn: 7 | output = conn.send_command("show version") 8 | # as ssh2net always returns a list of outputs, pass the zeroith element of the output 9 | output = conn.textfsm_parse_output("show version", output[0]) 10 | print(output) 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | target-version = ['py37'] 4 | include = '\.pyi?$' 5 | exclude = ''' 6 | ( 7 | /( 8 | \.eggs 9 | | \.git 10 | | \.hg 11 | | \.mypy_cache 12 | | \.tox 13 | | venv 14 | )/ 15 | ) 16 | ''' 17 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | tox>=3.14.0 2 | black>=19.3b0 3 | pytest>=5.0.1 4 | pytest-cov>=2.7.1 5 | pyfakefs>=3.6 6 | pylama>=7.6.6 7 | pycodestyle>=2.5.0 8 | pydocstyle>=4.0.1 9 | pylint>=2.3.1 10 | darglint>=1.1.0 11 | pdoc3>=0.6.2 12 | netmiko>=2.4.1 13 | -r requirements.txt 14 | -r requirements-textfsm.txt 15 | -r requirements-paramiko.txt 16 | -------------------------------------------------------------------------------- /requirements-paramiko.txt: -------------------------------------------------------------------------------- 1 | paramiko>=2.6.0 -------------------------------------------------------------------------------- /requirements-textfsm.txt: -------------------------------------------------------------------------------- 1 | textfsm>=1.1.0 2 | ntc_templates>=1.1.0 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ssh2-python>=0.18.0-1 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pylama] 2 | linters = mccabe,pycodestyle,pylint 3 | skip = tests/*,.tox/*,venv/*,build/*,private/*,comparison_tests/*,examples/* 4 | 5 | [pylama:pycodestyle] 6 | max_line_length = 100 7 | 8 | [pylama:pylint] 9 | rcfile = .pylintrc 10 | 11 | [pydocstyle] 12 | ignore = D101,D202,D212,D400,D406,D407,D408,D409,D415 13 | match-dir = ^ssh2net/* 14 | 15 | [darglint] 16 | docstring_style = google 17 | 18 | [mypy] 19 | python_version = 3.7 20 | strict_optional = False 21 | disallow_any_generics = False 22 | ignore_missing_imports = True 23 | warn_redundant_casts = True 24 | warn_unused_configs = True 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ssh2net network ssh client library""" 3 | import setuptools 4 | 5 | 6 | __author__ = "Carl Montanari" 7 | 8 | with open("README.md", "r") as f: 9 | README = f.read() 10 | 11 | setuptools.setup( 12 | name="ssh2net", 13 | version="2020.01.10", 14 | author=__author__, 15 | author_email="carl.r.montanari@gmail.com", 16 | description="SSH client for network devices built on ssh2-python", 17 | long_description=README, 18 | long_description_content_type="text/markdown", 19 | url="https://github.com/carlmontanari/ssh2net", 20 | packages=setuptools.find_packages(), 21 | install_requires=["ssh2-python>=0.18.0-1"], 22 | extras_require={ 23 | "textfsm": ["textfsm>=1.1.0", "ntc-templates>=1.1.0"], 24 | "paramiko": ["paramiko>=2.6.0"], 25 | }, 26 | classifiers=[ 27 | "License :: OSI Approved :: MIT License", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.6", 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.8", 32 | "Operating System :: POSIX :: Linux", 33 | "Operating System :: MacOS", 34 | ], 35 | python_requires=">=3.6", 36 | ) 37 | -------------------------------------------------------------------------------- /ssh2net/__init__.py: -------------------------------------------------------------------------------- 1 | """ssh2net network ssh client library""" 2 | import logging 3 | from logging import NullHandler 4 | 5 | from ssh2net.base import SSH2Net 6 | from ssh2net.channel import SSH2NetChannel 7 | from ssh2net.session import SSH2NetSession 8 | from ssh2net.netmiko_compatibility import connect_handler as ConnectHandler 9 | from ssh2net.ssh_config import SSH2NetSSHConfig 10 | from ssh2net.core.driver import BaseNetworkDriver 11 | from ssh2net.core.cisco_iosxe.driver import IOSXEDriver 12 | from ssh2net.core.cisco_nxos.driver import NXOSDriver 13 | from ssh2net.core.cisco_iosxr.driver import IOSXRDriver 14 | from ssh2net.core.arista_eos.driver import EOSDriver 15 | from ssh2net.core.juniper_junos.driver import JunosDriver 16 | 17 | __version__ = "2020.01.10" 18 | __all__ = ( 19 | "SSH2Net", 20 | "SSH2NetSession", 21 | "SSH2NetChannel", 22 | "SSH2NetSSHConfig", 23 | "ConnectHandler", 24 | "BaseNetworkDriver", 25 | "IOSXEDriver", 26 | "NXOSDriver", 27 | "IOSXRDriver", 28 | "EOSDriver", 29 | "JunosDriver", 30 | ) 31 | 32 | 33 | # Class to filter duplicate log entries for the channel logger 34 | # Stolen from: https://stackoverflow.com/questions/44691558/ \ 35 | # suppress-multiple-messages-with-same-content-in-python-logging-module-aka-log-co 36 | class DuplicateFilter(logging.Filter): 37 | def filter(self, record): 38 | # Fields to compare to previous log entry if these fields match; skip log entry 39 | current_log = (record.module, record.levelno, record.msg) 40 | if current_log != getattr(self, "last_log", None): 41 | self.last_log = current_log 42 | return True 43 | return False 44 | 45 | 46 | # Setup session logger 47 | session_log = logging.getLogger(f"{__name__}_session") 48 | logging.getLogger(f"{__name__}_session").addHandler(NullHandler()) 49 | 50 | # Setup channel logger 51 | channel_log = logging.getLogger(f"{__name__}_channel") 52 | # Add duplicate filter to channel log 53 | channel_log.addFilter(DuplicateFilter()) 54 | logging.getLogger(f"{__name__}_channel").addHandler(NullHandler()) 55 | -------------------------------------------------------------------------------- /ssh2net/community/__init__.py: -------------------------------------------------------------------------------- 1 | """ssh2net community platform drivers""" 2 | -------------------------------------------------------------------------------- /ssh2net/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ssh2net core platform drivers""" 2 | -------------------------------------------------------------------------------- /ssh2net/core/arista_eos/__init__.py: -------------------------------------------------------------------------------- 1 | """ssh2net arista eos driver""" 2 | -------------------------------------------------------------------------------- /ssh2net/core/arista_eos/driver.py: -------------------------------------------------------------------------------- 1 | """ssh2net.core.arista_eos.driver""" 2 | import re 3 | from typing import Any, Dict 4 | 5 | from ssh2net.core.driver import BaseNetworkDriver, PrivilegeLevel 6 | 7 | 8 | EOS_ARG_MAPPER = { 9 | "comms_prompt_regex": r"^[a-z0-9.\-@()/:]{1,32}[#>$]$", 10 | "comms_return_char": "\n", 11 | "comms_pre_login_handler": "", 12 | "comms_disable_paging": "terminal length 0", 13 | } 14 | 15 | PRIVS = { 16 | "exec": ( 17 | PrivilegeLevel( 18 | re.compile(r"^[a-z0-9.\-@()/:]{1,32}>$", flags=re.M | re.I), 19 | "exec", 20 | None, 21 | None, 22 | "privilege_exec", 23 | "enable", 24 | True, 25 | "Password:", 26 | True, 27 | 0, 28 | ) 29 | ), 30 | "privilege_exec": ( 31 | PrivilegeLevel( 32 | re.compile(r"^[a-z0-9.\-@/:]{1,32}#$", flags=re.M | re.I), 33 | "privilege_exec", 34 | "exec", 35 | "disable", 36 | "configuration", 37 | "configure terminal", 38 | False, 39 | False, 40 | True, 41 | 1, 42 | ) 43 | ), 44 | "configuration": ( 45 | PrivilegeLevel( 46 | re.compile(r"^[a-z0-9.\-@/:]{1,32}\(config\)#$", flags=re.M | re.I), 47 | "configuration", 48 | "priv", 49 | "end", 50 | None, 51 | None, 52 | False, 53 | False, 54 | True, 55 | 2, 56 | ) 57 | ), 58 | "special_configuration": ( 59 | PrivilegeLevel( 60 | re.compile(r"^[a-z0-9.\-@/:]{1,32}\(config[a-z0-9.\-@/:]{1,16}\)#$", flags=re.M | re.I), 61 | "special_configuration", 62 | "priv", 63 | "end", 64 | None, 65 | None, 66 | False, 67 | False, 68 | False, 69 | 3, 70 | ) 71 | ), 72 | } 73 | 74 | 75 | class EOSDriver(BaseNetworkDriver): 76 | def __init__(self, **kwargs: Dict[str, Any]): 77 | """ 78 | Initialize SSH2Net EOSDriver Object 79 | 80 | Args: 81 | **kwargs: keyword args to pass to inherited class(es) 82 | 83 | Returns: 84 | N/A # noqa 85 | 86 | Raises: 87 | N/A # noqa 88 | """ 89 | super().__init__(**kwargs) 90 | self.privs = PRIVS 91 | self.default_desired_priv = "privilege_exec" 92 | self.textfsm_platform = "arista_eos" 93 | -------------------------------------------------------------------------------- /ssh2net/core/cisco_iosxe/__init__.py: -------------------------------------------------------------------------------- 1 | """ssh2net cisco iosxe driver""" 2 | -------------------------------------------------------------------------------- /ssh2net/core/cisco_iosxe/driver.py: -------------------------------------------------------------------------------- 1 | """ssh2net.core.cisco_iosxe.driver""" 2 | import re 3 | from typing import Any, Dict 4 | 5 | from ssh2net.core.driver import BaseNetworkDriver, PrivilegeLevel 6 | 7 | 8 | IOSXE_ARG_MAPPER = { 9 | "comms_prompt_regex": r"^[a-z0-9.\-@()/:]{1,32}[#>$]$", 10 | "comms_return_char": "\n", 11 | "comms_pre_login_handler": "", 12 | "comms_disable_paging": "terminal length 0", 13 | } 14 | 15 | PRIVS = { 16 | "exec": ( 17 | PrivilegeLevel( 18 | re.compile(r"^[a-z0-9.\-@()/:]{1,32}>$", flags=re.M | re.I), 19 | "exec", 20 | None, 21 | None, 22 | "privilege_exec", 23 | "enable", 24 | True, 25 | "Password:", 26 | True, 27 | 0, 28 | ) 29 | ), 30 | "privilege_exec": ( 31 | PrivilegeLevel( 32 | re.compile(r"^[a-z0-9.\-@/:]{1,32}#$", flags=re.M | re.I), 33 | "privilege_exec", 34 | "exec", 35 | "disable", 36 | "configuration", 37 | "configure terminal", 38 | False, 39 | False, 40 | True, 41 | 1, 42 | ) 43 | ), 44 | "configuration": ( 45 | PrivilegeLevel( 46 | re.compile(r"^[a-z0-9.\-@/:]{1,32}\(config\)#$", flags=re.M | re.I), 47 | "configuration", 48 | "priv", 49 | "end", 50 | None, 51 | None, 52 | False, 53 | False, 54 | True, 55 | 2, 56 | ) 57 | ), 58 | "special_configuration": ( 59 | PrivilegeLevel( 60 | re.compile(r"^[a-z0-9.\-@/:]{1,32}\(config[a-z0-9.\-@/:]{1,16}\)#$", flags=re.M | re.I), 61 | "special_configuration", 62 | "priv", 63 | "end", 64 | None, 65 | None, 66 | False, 67 | False, 68 | False, 69 | 3, 70 | ) 71 | ), 72 | } 73 | 74 | 75 | class IOSXEDriver(BaseNetworkDriver): 76 | def __init__(self, **kwargs: Dict[str, Any]): 77 | """ 78 | Initialize SSH2Net IOSXEDriver Object 79 | 80 | Args: 81 | **kwargs: keyword args to pass to inherited class(es) 82 | 83 | Returns: 84 | N/A # noqa 85 | 86 | Raises: 87 | N/A # noqa 88 | """ 89 | super().__init__(**kwargs) 90 | self.privs = PRIVS 91 | self.default_desired_priv = "privilege_exec" 92 | self.textfsm_platform = "cisco_ios" 93 | -------------------------------------------------------------------------------- /ssh2net/core/cisco_iosxr/__init__.py: -------------------------------------------------------------------------------- 1 | """ssh2net cisco iosxr driver""" 2 | -------------------------------------------------------------------------------- /ssh2net/core/cisco_iosxr/driver.py: -------------------------------------------------------------------------------- 1 | """ssh2net.core.cisco_iosxr.driver""" 2 | import re 3 | import time 4 | from typing import Any, Dict 5 | 6 | from ssh2net.core.driver import BaseNetworkDriver, PrivilegeLevel 7 | 8 | 9 | IOSXR_ARG_MAPPER = { 10 | "comms_prompt_regex": r"^[a-z0-9.\-@()/:]{1,32}[#>$]$", 11 | "comms_return_char": "\n", 12 | "comms_pre_login_handler": "", 13 | "comms_disable_paging": "terminal length 0", 14 | } 15 | 16 | PRIVS = { 17 | "privilege_exec": ( 18 | PrivilegeLevel( 19 | re.compile(r"^[a-z0-9.\-@/:]{1,32}#$", flags=re.M | re.I), 20 | "privilege_exec", 21 | "exec", 22 | "disable", 23 | "configuration", 24 | "configure terminal", 25 | False, 26 | False, 27 | True, 28 | 1, 29 | ) 30 | ), 31 | "configuration": ( 32 | PrivilegeLevel( 33 | re.compile(r"^[a-z0-9.\-@/:]{1,32}\(config\)#$", flags=re.M | re.I), 34 | "configuration", 35 | "priv", 36 | "end", 37 | None, 38 | None, 39 | False, 40 | False, 41 | True, 42 | 2, 43 | ) 44 | ), 45 | "special_configuration": ( 46 | PrivilegeLevel( 47 | re.compile(r"^[a-z0-9.\-@/:]{1,32}\(config[a-z0-9.\-@/:]{1,16}\)#$", flags=re.M | re.I), 48 | "special_configuration", 49 | "priv", 50 | "end", 51 | None, 52 | None, 53 | False, 54 | False, 55 | False, 56 | 3, 57 | ) 58 | ), 59 | } 60 | 61 | 62 | def comms_pre_login_handler(cls): # pylint: disable=W0613 63 | # sleep for session to establish; without this we never find base prompt 64 | time.sleep(1) 65 | 66 | 67 | class IOSXRDriver(BaseNetworkDriver): 68 | def __init__(self, **kwargs: Dict[str, Any]): 69 | """ 70 | Initialize SSH2Net IOSXRDriver Object 71 | 72 | Args: 73 | **kwargs: keyword args to pass to inherited class(es) 74 | 75 | Returns: 76 | N/A # noqa 77 | 78 | Raises: 79 | N/A # noqa 80 | """ 81 | super().__init__(**kwargs) 82 | self.privs = PRIVS 83 | self.default_desired_priv = "privilege_exec" 84 | self.textfsm_platform = "cisco_xr" 85 | -------------------------------------------------------------------------------- /ssh2net/core/cisco_nxos/__init__.py: -------------------------------------------------------------------------------- 1 | """ssh2net cisco nxos driver""" 2 | -------------------------------------------------------------------------------- /ssh2net/core/cisco_nxos/driver.py: -------------------------------------------------------------------------------- 1 | """ssh2net.core.cisco_nxos.driver""" 2 | import re 3 | from typing import Any, Dict 4 | 5 | from ssh2net.core.driver import BaseNetworkDriver, PrivilegeLevel 6 | 7 | 8 | NXOS_ARG_MAPPER = { 9 | "comms_prompt_regex": r"^[a-z0-9.\-@()/:]{1,32}[#>$]$", 10 | "comms_return_char": "\n", 11 | "comms_pre_login_handler": "", 12 | "comms_disable_paging": "terminal length 0", 13 | } 14 | 15 | PRIVS = { 16 | "exec": ( 17 | PrivilegeLevel( 18 | re.compile(r"^[a-z0-9.\-@()/:]{1,32}>$", flags=re.M | re.I), 19 | "exec", 20 | None, 21 | None, 22 | "privilege_exec", 23 | "enable", 24 | True, 25 | "Password:", 26 | True, 27 | 0, 28 | ) 29 | ), 30 | "privilege_exec": ( 31 | PrivilegeLevel( 32 | re.compile(r"^[a-z0-9.\-@/:]{1,32}#$", flags=re.M | re.I), 33 | "privilege_exec", 34 | "exec", 35 | "disable", 36 | "configuration", 37 | "configure terminal", 38 | False, 39 | False, 40 | True, 41 | 1, 42 | ) 43 | ), 44 | "configuration": ( 45 | PrivilegeLevel( 46 | re.compile(r"^[a-z0-9.\-@/:]{1,32}\(config\)#$", flags=re.M | re.I), 47 | "configuration", 48 | "priv", 49 | "end", 50 | None, 51 | None, 52 | False, 53 | False, 54 | True, 55 | 2, 56 | ) 57 | ), 58 | "special_configuration": ( 59 | PrivilegeLevel( 60 | re.compile(r"^[a-z0-9.\-@/:]{1,32}\(config[a-z0-9.\-@/:]{1,16}\)#$", flags=re.M | re.I), 61 | "special_configuration", 62 | "priv", 63 | "end", 64 | None, 65 | None, 66 | False, 67 | False, 68 | False, 69 | 3, 70 | ) 71 | ), 72 | } 73 | 74 | 75 | class NXOSDriver(BaseNetworkDriver): 76 | def __init__(self, **kwargs: Dict[str, Any]): 77 | """ 78 | Initialize SSH2Net NXOSDriver Object 79 | 80 | Args: 81 | **kwargs: keyword args to pass to inherited class(es) 82 | 83 | Returns: 84 | N/A # noqa 85 | 86 | Raises: 87 | N/A # noqa 88 | """ 89 | super().__init__(**kwargs) 90 | self.privs = PRIVS 91 | self.default_desired_priv = "privilege_exec" 92 | -------------------------------------------------------------------------------- /ssh2net/core/driver.py: -------------------------------------------------------------------------------- 1 | """ssh2net.core.driver""" 2 | import collections 3 | import re 4 | from typing import Any, Dict, Optional, Union 5 | 6 | from ssh2net.base import SSH2Net 7 | from ssh2net.exceptions import UnknownPrivLevel 8 | from ssh2net.helper import _textfsm_get_template, textfsm_parse 9 | 10 | 11 | PrivilegeLevel = collections.namedtuple( 12 | "PrivLevel", 13 | "pattern " 14 | "name " 15 | "deescalate_priv " 16 | "deescalate " 17 | "escalate_priv " 18 | "escalate " 19 | "escalate_auth " 20 | "escalate_prompt " 21 | "requestable " 22 | "level", 23 | ) 24 | 25 | PRIVS = {} 26 | 27 | 28 | class BaseNetworkDriver(SSH2Net): 29 | def __init__(self, auth_secondary: Optional[Union[str]] = None, **kwargs: Dict[str, Any]): 30 | """ 31 | Initialize SSH2Net BaseNetworkDriver Object 32 | 33 | Args: 34 | auth_secondary: password to use for secondary authentication (enable) 35 | 36 | Returns: 37 | N/A # noqa 38 | 39 | Raises: 40 | N/A # noqa 41 | """ 42 | self.auth_secondary = auth_secondary 43 | super().__init__(**kwargs) 44 | self.privs = PRIVS 45 | self.default_desired_priv = None 46 | self.textfsm_platform = None 47 | 48 | def _determine_current_priv(self, current_prompt: str): 49 | """ 50 | Determine current privilege level from prompt string 51 | 52 | Args: 53 | current_prompt: string of current prompt 54 | 55 | Returns: 56 | priv_level: NamedTuple of current privilege level 57 | 58 | Raises: 59 | UnknownPrivLevel: if privilege level cannot be determined # noqa 60 | # darglint raises DAR401 for some reason hence the noqa... 61 | 62 | """ 63 | for priv_level in self.privs.values(): 64 | if re.search(priv_level.pattern, current_prompt): 65 | return priv_level 66 | raise UnknownPrivLevel 67 | 68 | def _escalate(self) -> None: 69 | """ 70 | Escalate to the next privilege level up 71 | 72 | Args: 73 | N/A # noqa 74 | 75 | Returns: 76 | N/A # noqa 77 | 78 | Raises: 79 | N/A # noqa 80 | 81 | """ 82 | current_priv = self._determine_current_priv(self.get_prompt()) 83 | if current_priv.escalate: 84 | if current_priv.escalate_auth: 85 | self.send_inputs_interact( 86 | ( 87 | current_priv.escalate, 88 | current_priv.escalate_prompt, 89 | self.auth_enable, 90 | self.privs.get("escalate_priv"), 91 | ), 92 | hidden_response=True, 93 | ) 94 | else: 95 | self.send_inputs(current_priv.escalate) 96 | 97 | def _deescalate(self) -> None: 98 | """ 99 | Deescalate to the next privilege level down 100 | 101 | Args: 102 | N/A # noqa 103 | 104 | Returns: 105 | N/A # noqa 106 | 107 | Raises: 108 | N/A # noqa 109 | 110 | """ 111 | current_priv = self._determine_current_priv(self.get_prompt()) 112 | if current_priv.deescalate: 113 | self.send_inputs(current_priv.deescalate) 114 | 115 | def attain_priv(self, desired_priv) -> None: 116 | """ 117 | Attain desired priv level 118 | 119 | Args: 120 | desired_priv: string name of desired privilege level 121 | (see ssh2net.core..driver for levels) 122 | 123 | Returns: 124 | N/A # noqa 125 | 126 | Raises: 127 | N/A # noqa 128 | 129 | """ 130 | while True: 131 | current_priv = self._determine_current_priv(self.get_prompt()) 132 | if current_priv == self.privs[desired_priv]: 133 | return 134 | if current_priv.level > self.privs[desired_priv].level: 135 | self._deescalate() 136 | else: 137 | self._escalate() 138 | 139 | def send_command(self, commands): 140 | """ 141 | Send command(s) 142 | 143 | Args: 144 | commands: string or list of strings to send to device in privilege exec mode 145 | 146 | Returns: 147 | N/A # noqa 148 | 149 | Raises: 150 | N/A # noqa 151 | """ 152 | self.attain_priv(self.default_desired_priv) 153 | result = self.send_inputs(commands) 154 | return result 155 | 156 | def send_config_set(self, configs): 157 | """ 158 | Send configuration(s) 159 | 160 | Args: 161 | configs: string or list of strings to send to device in config mode 162 | 163 | Returns: 164 | N/A # noqa 165 | 166 | Raises: 167 | N/A # noqa 168 | """ 169 | self.attain_priv("configuration") 170 | result = self.send_inputs(configs) 171 | self.attain_priv(self.default_desired_priv) 172 | return result 173 | 174 | def textfsm_parse_output(self, command: str, output: str) -> str: 175 | """ 176 | Parse output with TextFSM and ntc-templates 177 | 178 | Args: 179 | command: command used to get output 180 | output: output from command 181 | 182 | Returns: 183 | output: parsed output 184 | 185 | Raises: 186 | N/A # noqa 187 | """ 188 | template = _textfsm_get_template(self.textfsm_platform, command) 189 | if template: 190 | output = textfsm_parse(template, output) 191 | return output 192 | -------------------------------------------------------------------------------- /ssh2net/core/juniper_junos/__init__.py: -------------------------------------------------------------------------------- 1 | """ssh2net juniper junos driver""" 2 | -------------------------------------------------------------------------------- /ssh2net/core/juniper_junos/driver.py: -------------------------------------------------------------------------------- 1 | """ssh2net.core.juniper_junos.driver""" 2 | import re 3 | from typing import Any, Dict 4 | 5 | from ssh2net.core.driver import BaseNetworkDriver, PrivilegeLevel 6 | 7 | 8 | JUNOS_ARG_MAPPER = { 9 | "comms_prompt_regex": r"^[a-z0-9.\-@()/:]{1,32}[#>$]$", 10 | "comms_return_char": "\n", 11 | "comms_pre_login_handler": "", 12 | "comms_disable_paging": "ssh2net.core.juniper_junos.helper.disable_paging", 13 | } 14 | 15 | PRIVS = { 16 | "exec": ( 17 | PrivilegeLevel( 18 | re.compile(r"^[a-z0-9.\-@()/:]{1,32}>$", flags=re.M | re.I), 19 | "exec", 20 | None, 21 | None, 22 | "configuration", 23 | "configure", 24 | False, 25 | False, 26 | True, 27 | 0, 28 | ) 29 | ), 30 | "configuration": ( 31 | PrivilegeLevel( 32 | re.compile(r"^[a-z0-9.\-@()/:]{1,32}#$", flags=re.M | re.I), 33 | "configuration", 34 | "exec", 35 | "exit configuration-mode", 36 | None, 37 | None, 38 | False, 39 | False, 40 | True, 41 | 1, 42 | ) 43 | ), 44 | } 45 | 46 | 47 | class JunosDriver(BaseNetworkDriver): 48 | def __init__(self, **kwargs: Dict[str, Any]): 49 | """ 50 | Initialize SSH2Net IOSXEDriver Object 51 | 52 | Args: 53 | **kwargs: keyword args to pass to inherited class(es) 54 | 55 | Returns: 56 | N/A # noqa 57 | 58 | Raises: 59 | N/A # noqa 60 | """ 61 | super().__init__(**kwargs) 62 | self.privs = PRIVS 63 | self.default_desired_priv = "exec" 64 | -------------------------------------------------------------------------------- /ssh2net/core/juniper_junos/helper.py: -------------------------------------------------------------------------------- 1 | """ssh2net.core.juniper_junos.helper""" 2 | 3 | 4 | def disable_paging(cls): 5 | """ 6 | Disable paging and set screen width for Junos 7 | 8 | Args: 9 | cls: SSH2Net connection object 10 | 11 | Returns: 12 | N/A # noqa 13 | 14 | Raises: 15 | N/A # noqa 16 | """ 17 | cls.send_inputs("set cli screen-length 0") 18 | cls.send_inputs("set cli screen-width 511") 19 | -------------------------------------------------------------------------------- /ssh2net/decorators.py: -------------------------------------------------------------------------------- 1 | """ssh2net.decorators""" 2 | import logging 3 | import multiprocessing.pool 4 | import time 5 | 6 | 7 | channel_log = logging.getLogger("ssh2net_channel") 8 | 9 | 10 | def operation_timeout(attribute): 11 | """ 12 | Decorate an "operation" -- raises exception if the operation timeout is exceeded 13 | 14 | This decorator wraps an entire "operation" -- in other words for each send_input or 15 | send_inputs_interact this decorator wraps that entire operation. The `comms_operation_timeout` 16 | value governs this timeout decorator -- and as such it should always be larger than the base 17 | `session_timeout` argument plus the amount of time that `channel_timeout` decorator will delay 18 | while retrying the read operation. 19 | 20 | Note: Different versions for Windows vs POSIX systems as I think the Windows version is ugly 21 | and would prefer to not use it if possible. I imagine there is a better way to implement 22 | a OS dependant decorator, but for now this works! 23 | 24 | Args: 25 | attribute: attribute to inspect in class (to set timeout duration) 26 | 27 | Returns: 28 | decorate: wrapped function 29 | 30 | Raises: 31 | TimeoutError: if timeout exceeded 32 | 33 | """ 34 | import signal # noqa 35 | 36 | def _raise_exception(signum, frame): 37 | raise TimeoutError 38 | 39 | def decorate(wrapped_func): 40 | def timeout_wrapper(self, *args, **kwargs): 41 | timeout_duration = getattr(self, attribute) 42 | if not timeout_duration: 43 | return wrapped_func 44 | old = signal.signal(signal.SIGALRM, _raise_exception) 45 | signal.setitimer(signal.ITIMER_REAL, timeout_duration) 46 | try: 47 | return wrapped_func(self, *args, **kwargs) 48 | finally: 49 | if timeout_duration: 50 | signal.setitimer(signal.ITIMER_REAL, 0) 51 | signal.signal(signal.SIGALRM, old) 52 | 53 | return timeout_wrapper 54 | 55 | return decorate 56 | 57 | 58 | def operation_timeout_win(attribute): 59 | """ 60 | Decorate an "operation" -- raises exception if the operation timeout is exceeded 61 | 62 | See documentation for "operation_timeout" 63 | 64 | This is a variation of the "operation_timeout" decorator that supports windows. I think the 65 | Windows version is ugly and would prefer to not use it if possible, so the `ssh2net.channel` 66 | module checks for os type and imports the appropriate version to not use this if it can at all 67 | be avoided. 68 | 69 | Args: 70 | attribute: attribute to inspect in class (to set timeout duration) 71 | 72 | Returns: 73 | decorate: wrapped function 74 | 75 | Raises: 76 | TimeoutError: if timeout exceeded 77 | 78 | """ 79 | 80 | def decorate(wrapped_func): 81 | def timeout_wrapper(self, *args, **kwargs): 82 | timeout_duration = getattr(self, attribute) 83 | pool = multiprocessing.pool.ThreadPool(processes=1) 84 | args = [self, *args] 85 | async_result = pool.apply_async(wrapped_func, args, kwargs) 86 | try: 87 | return async_result.get(timeout_duration) 88 | except multiprocessing.context.TimeoutError: 89 | raise TimeoutError 90 | 91 | return timeout_wrapper 92 | 93 | return decorate 94 | 95 | 96 | def channel_timeout(exception_to_check, attempts=5, starting_delay=0.1, backoff=2): 97 | """ 98 | Decorate read operations; basic backoff timer to try to read channel for reasonable time 99 | 100 | This decorator wraps individual read operations. If/when the read operation times out 101 | (timeout configured by `session_timeout`), run a basic backoff process retrying five times 102 | 103 | Args: 104 | exception_to_check: Exception to handle; basically if this exception is seen, try again 105 | attempts: number of attempts to make to retry 106 | starting_delay: initial backoff delay 107 | backoff: backoff multiplier 108 | 109 | Returns: 110 | decorate: wrapped function 111 | 112 | Raises: 113 | N/A # noqa 114 | 115 | """ 116 | 117 | def decorate(wrapped_func): 118 | def retry_wrapper(self, *args, **kwargs): 119 | attempt, delay = attempts, starting_delay 120 | while attempt > 1: 121 | try: 122 | return wrapped_func(self, *args, **kwargs) 123 | except exception_to_check: 124 | channel_log.info(f"Retrying read operation in {delay} seconds...") 125 | time.sleep(delay) 126 | attempt -= 1 127 | delay *= backoff 128 | return wrapped_func(self, *args, **kwargs) 129 | 130 | return retry_wrapper 131 | 132 | return decorate 133 | -------------------------------------------------------------------------------- /ssh2net/exceptions.py: -------------------------------------------------------------------------------- 1 | """ssh2net.exceptions""" 2 | 3 | 4 | class ValidationError(Exception): 5 | pass 6 | 7 | 8 | class RequirementsNotSatisfied(Exception): 9 | pass 10 | 11 | 12 | class AuthenticationFailed(Exception): 13 | pass 14 | 15 | 16 | class SetupTimeout(Exception): 17 | pass 18 | 19 | 20 | class UnknownPrivLevel(Exception): 21 | pass 22 | -------------------------------------------------------------------------------- /ssh2net/helper.py: -------------------------------------------------------------------------------- 1 | """ssh2net.helper""" 2 | import importlib 3 | from io import TextIOWrapper 4 | import pkg_resources # pylint: disable=C0411 5 | import warnings 6 | 7 | 8 | def validate_external_function(possible_function): 9 | """ 10 | Validate string representing external function is a callable 11 | 12 | Args: 13 | possible_function: string "pointing" to external function 14 | 15 | Returns: 16 | None/Callable: None or callable function 17 | 18 | Raises: 19 | N/A # noqa 20 | 21 | """ 22 | try: 23 | if not isinstance(possible_function, str): 24 | return None 25 | if "." not in possible_function: 26 | return None 27 | ext_func_path = possible_function.split(".") 28 | ext_module = ".".join(ext_func_path[:-1]) 29 | ext_function = ext_func_path[-1] 30 | ext_module = importlib.import_module(ext_module) 31 | return getattr(ext_module, ext_function) 32 | except ModuleNotFoundError: 33 | return None 34 | 35 | 36 | def _textfsm_get_template(platform: str, command: str): 37 | """ 38 | Find correct TextFSM template based on platform and command executed 39 | 40 | Args: 41 | platform: ntc-templates device type; i.e. cisco_ios, arista_eos, etc. 42 | command: string of command that was executed (to find appropriate template) 43 | 44 | Returns: 45 | None or TextIOWrapper of opened template 46 | 47 | """ 48 | try: 49 | from textfsm.clitable import CliTable # noqa 50 | import ntc_templates # noqa 51 | except ModuleNotFoundError as exc: 52 | err = f"Module '{exc.name}' not installed!" 53 | msg = f"***** {err} {'*' * (80 - len(err))}" 54 | fix = ( 55 | f"To resolve this issue, install '{exc.name}'. You can do this in one of the following" 56 | " ways:\n" 57 | "1: 'pip install -r requirements-textfsm.txt'\n" 58 | "2: 'pip install ssh2net[textfsm]'" 59 | ) 60 | warning = "\n" + msg + "\n" + fix + "\n" + msg 61 | warnings.warn(warning) 62 | return None 63 | template_dir = pkg_resources.resource_filename("ntc_templates", "templates") 64 | cli_table = CliTable("index", template_dir) 65 | template_index = cli_table.index.GetRowMatch({"Platform": platform, "Command": command}) 66 | if not template_index: 67 | return None 68 | template_name = cli_table.index.index[template_index]["Template"] 69 | template = open(f"{template_dir}/{template_name}") 70 | return template 71 | 72 | 73 | def textfsm_parse(template, output): 74 | """ 75 | Parse output with TextFSM and ntc-templates, try to return structured output 76 | 77 | Args: 78 | template: TextIOWrapper or string path to template to use to parse data 79 | output: unstructured output from device to parse 80 | 81 | Returns: 82 | output: structured data 83 | 84 | """ 85 | import textfsm # noqa 86 | 87 | if not isinstance(template, TextIOWrapper): 88 | template = open(template) 89 | re_table = textfsm.TextFSM(template) 90 | try: 91 | output = re_table.ParseText(output) 92 | return output 93 | except textfsm.parser.TextFSMError: 94 | pass 95 | return output 96 | -------------------------------------------------------------------------------- /ssh2net/netmiko_compatibility.py: -------------------------------------------------------------------------------- 1 | """ssh2net.netmiko_compatibility""" 2 | from ssh2net.core.cisco_iosxe.driver import IOSXEDriver, IOSXE_ARG_MAPPER 3 | from ssh2net.core.cisco_nxos.driver import NXOSDriver, NXOS_ARG_MAPPER 4 | from ssh2net.core.cisco_iosxr.driver import IOSXRDriver, IOSXR_ARG_MAPPER 5 | from ssh2net.core.arista_eos.driver import EOSDriver, EOS_ARG_MAPPER 6 | from ssh2net.core.juniper_junos.driver import JunosDriver, JUNOS_ARG_MAPPER 7 | 8 | NETMIKO_DEVICE_TYPE_MAPPER = { 9 | "cisco_ios": {"driver": IOSXEDriver, "arg_mapper": IOSXE_ARG_MAPPER}, 10 | "cisco_xe": {"driver": IOSXEDriver, "arg_mapper": IOSXE_ARG_MAPPER}, 11 | "cisco_nxos": {"driver": NXOSDriver, "arg_mapper": NXOS_ARG_MAPPER}, 12 | "cisco_xr": {"driver": IOSXRDriver, "arg_mapper": IOSXR_ARG_MAPPER}, 13 | "arista_eos": {"driver": EOSDriver, "arg_mapper": EOS_ARG_MAPPER}, 14 | "juniper_junos": {"driver": JunosDriver, "arg_mapper": JUNOS_ARG_MAPPER}, 15 | } 16 | 17 | VALID_SSH2NET_KWARGS = { 18 | "setup_host", 19 | "setup_validate_host", 20 | "setup_port", 21 | "setup_timeout", 22 | "setup_ssh_config_file", 23 | "session_keepalive", 24 | "session_keepalive_interval", 25 | "session_timeout", 26 | "auth_user", 27 | "auth_password", 28 | "auth_public_key", 29 | "comms_prompt_regex", 30 | "comms_operation_timeout", 31 | "comms_return_char", 32 | "comms_pre_login_handler", 33 | "comms_disable_paging", 34 | } 35 | 36 | 37 | def connect_handler(auto_open=True, **kwargs): 38 | """ 39 | Convert netmiko style "ConnectHandler" device creation to SSH2Net style 40 | 41 | Args: 42 | auto_open: auto open connection or not (primarily for testing purposes) 43 | **kwargs: keyword arguments 44 | 45 | Returns: 46 | driver: SSH2Net connection object for specified device-type 47 | 48 | Raises: 49 | TypeError: if unsupported netmiko device type is provided 50 | 51 | """ 52 | if kwargs["device_type"] not in NETMIKO_DEVICE_TYPE_MAPPER.keys(): 53 | raise TypeError(f"Unsupported netmiko device type for ssh2net: {kwargs['device_type']}") 54 | 55 | driver_info = NETMIKO_DEVICE_TYPE_MAPPER.get(kwargs["device_type"]) 56 | driver_class = driver_info["driver"] 57 | driver_args = driver_info["arg_mapper"] 58 | kwargs.pop("device_type") 59 | 60 | transformed_kwargs = transform_netmiko_kwargs(kwargs) 61 | final_kwargs = {**transformed_kwargs, **driver_args} 62 | 63 | driver = driver_class(**final_kwargs) 64 | 65 | if auto_open: 66 | driver.open_shell() 67 | 68 | return driver 69 | 70 | 71 | def transform_netmiko_kwargs(kwargs): 72 | """ 73 | Transform netmiko style ConnectHandler arguments to ssh2net style 74 | 75 | Args: 76 | kwargs: netmiko-style ConnectHandler kwargs to transform to ssh2net style 77 | 78 | Returns: 79 | transformed_kwargs: converted keyword arguments 80 | 81 | """ 82 | host = kwargs.pop("host", None) 83 | ip = kwargs.pop("ip", None) 84 | kwargs["setup_host"] = host if host is not None else ip 85 | kwargs["setup_validate_host"] = False 86 | kwargs["setup_port"] = kwargs.pop("port", 22) 87 | kwargs["setup_timeout"] = 5 88 | kwargs["setup_ssh_config_file"] = kwargs.pop("ssh_config_file", False) 89 | kwargs["session_keepalive"] = False 90 | kwargs["session_keepalive_interval"] = 10 91 | if "global_delay_factor" in kwargs.keys(): 92 | kwargs["session_timeout"] = kwargs["global_delay_factor"] * 5000 93 | kwargs.pop("global_delay_factor") 94 | else: 95 | kwargs["session_timeout"] = 5000 96 | kwargs["auth_user"] = kwargs.pop("username") 97 | kwargs["auth_password"] = kwargs.pop("password", None) 98 | kwargs["auth_public_key"] = kwargs.pop("key_file", None) 99 | kwargs["comms_prompt_regex"] = "" 100 | kwargs["comms_operation_timeout"] = 10 101 | kwargs["comms_return_char"] = "" 102 | kwargs["comms_pre_login_handler"] = "" 103 | kwargs["comms_disable_paging"] = "" 104 | 105 | transformed_kwargs = {k: v for (k, v) in kwargs.items() if k in VALID_SSH2NET_KWARGS} 106 | 107 | return transformed_kwargs 108 | -------------------------------------------------------------------------------- /ssh2net/session_miko.py: -------------------------------------------------------------------------------- 1 | """ssh2net.session_miko""" 2 | import logging 3 | import time 4 | import warnings 5 | 6 | from paramiko.ssh_exception import AuthenticationException 7 | 8 | from ssh2net.exceptions import AuthenticationFailed, RequirementsNotSatisfied 9 | 10 | 11 | class SSH2NetSessionParamiko: 12 | def __init__(self, p_self): 13 | """ 14 | Initialize SSH2NetSessionParamiko Object 15 | 16 | This object, through composition, allows for using Paramiko as the underlying "driver" 17 | for SSH2Net instead of the default "ssh2-python". Paramiko will be ever so slightly slower 18 | but as you will most likely be I/O constrained it shouldn't matter! "ssh2-python" as of 19 | 20 October 2019 has a bug preventing keyboard interactive authentication from working as 20 | desired; this is the reason Paramiko is in here now! 21 | 22 | Args: 23 | p_self: SSH2Net object 24 | 25 | Returns: 26 | N/A # noqa 27 | 28 | Raises: 29 | N/A # noqa 30 | 31 | """ 32 | self.__dict__ = p_self.__dict__ 33 | self._session_alive = p_self._session_alive 34 | self._session_open = p_self._session_open 35 | self._channel_alive = p_self._channel_alive 36 | 37 | def _session_open_connect(self) -> None: 38 | """ 39 | Perform session handshake for paramiko (instead of default ssh2-python) 40 | 41 | Args: 42 | N/A # noqa 43 | 44 | Returns: 45 | N/A # noqa 46 | 47 | Raises: 48 | RequirementsNotSatisfied: if paramiko is not installed 49 | Exception: catch all for unknown exceptions during session handshake 50 | 51 | """ 52 | try: 53 | from paramiko import Transport # noqa 54 | except ModuleNotFoundError as exc: 55 | err = f"Module '{exc.name}' not installed!" 56 | msg = f"***** {err} {'*' * (80 - len(err))}" 57 | fix = ( 58 | f"To resolve this issue, install '{exc.name}'. You can do this in one of the " 59 | "following ways:\n" 60 | "1: 'pip install -r requirements-paramiko.txt'\n" 61 | "2: 'pip install ssh2net[paramiko]'" 62 | ) 63 | warning = "\n" + msg + "\n" + fix + "\n" + msg 64 | warnings.warn(warning) 65 | raise RequirementsNotSatisfied 66 | try: 67 | self.session = Transport(self.sock) 68 | self.session.start_client() 69 | self.session.set_timeout = self._set_timeout 70 | except Exception as exc: 71 | logging.critical( 72 | f"Failed to complete handshake with host {self.host}; " f"Exception: {exc}" 73 | ) 74 | raise exc 75 | 76 | def _session_public_key_auth(self) -> None: 77 | """ 78 | Perform public key based auth on SSH2NetSession 79 | 80 | Args: 81 | N/A # noqa 82 | 83 | Returns: 84 | N/A # noqa 85 | 86 | Raises: 87 | Exception: catch all for unhandled exceptions 88 | 89 | """ 90 | try: 91 | self.session.auth_publickey(self.auth_user, self.auth_public_key) 92 | except AuthenticationException: 93 | logging.critical(f"Public key authentication with host {self.host} failed.") 94 | except Exception as exc: 95 | logging.critical( 96 | "Unknown error occurred during public key authentication with host " 97 | f"{self.host}; Exception: {exc}" 98 | ) 99 | raise exc 100 | 101 | def _session_password_auth(self) -> None: 102 | """ 103 | Perform password or keyboard interactive based auth on SSH2NetSession 104 | 105 | Args: 106 | N/A # noqa 107 | 108 | Returns: 109 | N/A # noqa 110 | 111 | Raises: 112 | AuthenticationFailed: if authentication fails 113 | Exception: catch all for unknown other exceptions 114 | 115 | """ 116 | try: 117 | self.session.auth_password(self.auth_user, self.auth_password) 118 | except AuthenticationException as exc: 119 | logging.critical( 120 | f"Password authentication with host {self.host} failed. Exception: {exc}." 121 | "\n\tNote: Paramiko automatically attempts both standard auth as well as keyboard " 122 | "interactive auth. Paramiko exception about bad auth type may be misleading!" 123 | ) 124 | raise AuthenticationFailed 125 | except Exception as exc: 126 | logging.critical( 127 | "Unknown error occurred during password authentication with host " 128 | f"{self.host}; Exception: {exc}" 129 | ) 130 | raise exc 131 | 132 | def _channel_open_driver(self) -> None: 133 | """ 134 | Open channel 135 | 136 | Args: 137 | N/A # noqa 138 | 139 | Returns: 140 | N/A # noqa 141 | 142 | Raises: 143 | N/A # noqa 144 | 145 | """ 146 | self.channel = self.session.open_session() 147 | self.channel.get_pty() 148 | logging.debug(f"Channel to host {self.host} opened") 149 | 150 | def _channel_invoke_shell(self) -> None: 151 | """ 152 | Invoke shell on channel 153 | 154 | Additionally, this "re-points" some ssh2net method calls to the appropriate paramiko 155 | methods. This happens as ssh2net is primarily built on "ssh2-python" and there is not 156 | full parity between paramiko/ssh2-python. 157 | 158 | Args: 159 | N/A # noqa 160 | 161 | Returns: 162 | N/A # noqa 163 | 164 | Raises: 165 | N/A # noqa 166 | 167 | """ 168 | self._shell = True 169 | self.channel.invoke_shell() 170 | self.channel.read = self._paramiko_read_channel 171 | self.channel.write = self.channel.sendall 172 | self.session.set_blocking = self._set_blocking 173 | self.channel.flush = self._flush 174 | 175 | def _paramiko_read_channel(self): 176 | """ 177 | Patch channel.read method for paramiko driver 178 | 179 | "ssh2-python" returns a tuple of bytes and data, "paramiko" simply returns the data 180 | from the channel, patch this for parity with "ssh2-python". 181 | 182 | Args: 183 | N/A # noqa 184 | 185 | Returns: 186 | N/A # noqa 187 | 188 | Raises: 189 | N/A # noqa 190 | 191 | """ 192 | channel_read = self.channel.recv(1024) 193 | return None, channel_read 194 | 195 | def _flush(self): 196 | """ 197 | Patch a "flush" method for paramiko driver 198 | 199 | Need to investigate this further for two things: 200 | 1) is "flush" even necessary when using ssh2-python driver? 201 | 2) if it is necessary, is there a combination of reads/writes that would implement 202 | this in a sane fashion for paramiko 203 | 204 | Args: 205 | N/A # noqa 206 | 207 | Returns: 208 | N/A # noqa 209 | 210 | Raises: 211 | N/A # noqa 212 | 213 | """ 214 | while True: 215 | time.sleep(0.01) 216 | if self.channel.recv_ready(): 217 | self._paramiko_read_channel() 218 | else: 219 | self.channel.write("\n") 220 | return 221 | 222 | def _set_blocking(self, blocking): 223 | # Add docstring 224 | # need to reset timeout because it seems paramiko sets it to 0 if you set to non blocking 225 | # paramiko uses seconds instead of ms 226 | self.channel.setblocking(blocking) 227 | self.channel.settimeout(self.session_timeout / 1000) 228 | 229 | def _set_timeout(self, timeout): 230 | # paramiko uses seconds instead of ms 231 | self.channel.settimeout(timeout / 1000) 232 | -------------------------------------------------------------------------------- /ssh2net/session_ssh2.py: -------------------------------------------------------------------------------- 1 | """ssh2net.session_ssh2""" 2 | import logging 3 | 4 | from ssh2.session import Session 5 | from ssh2.exceptions import AuthenticationError 6 | 7 | from ssh2net.exceptions import AuthenticationFailed 8 | 9 | 10 | class SSH2NetSessionSSH2: 11 | def __init__(self, p_self): 12 | """ 13 | Initialize SSH2NetSessionSSH2 Object 14 | 15 | This is the default underlying "driver" for ssh2net. This has been pulled out of the 16 | "base" SSH2NetSession class to provide a mechanism for supporting both "ssh2-python" and 17 | "paramiko". 18 | 19 | Args: 20 | p_self: SSH2Net object 21 | 22 | Returns: 23 | N/A # noqa 24 | 25 | Raises: 26 | N/A # noqa 27 | 28 | """ 29 | self.__dict__ = p_self.__dict__ 30 | self._session_alive = p_self._session_alive 31 | self._session_open = p_self._session_open 32 | self._channel_alive = p_self._channel_alive 33 | 34 | def _session_open_connect(self) -> None: 35 | """ 36 | Perform session handshake 37 | 38 | Args: 39 | N/A # noqa 40 | 41 | Returns: 42 | N/A # noqa 43 | 44 | Raises: 45 | Exception: catch all for unknown exceptions during session handshake 46 | 47 | """ 48 | self.session = Session() 49 | if self.session_timeout: 50 | self.session.set_timeout(self.session_timeout) 51 | try: 52 | self.session.handshake(self.sock) 53 | except Exception as exc: 54 | logging.critical( 55 | f"Failed to complete handshake with host {self.host}; " f"Exception: {exc}" 56 | ) 57 | raise exc 58 | 59 | def _session_public_key_auth(self) -> None: 60 | """ 61 | Perform public key based auth on SSH2NetSession 62 | 63 | Args: 64 | N/A # noqa 65 | 66 | Returns: 67 | N/A # noqa 68 | 69 | Raises: 70 | Exception: catch all for unhandled exceptions 71 | 72 | """ 73 | try: 74 | self.session.userauth_publickey_fromfile(self.auth_user, self.auth_public_key) 75 | except AuthenticationError: 76 | logging.critical(f"Public key authentication with host {self.host} failed. ") 77 | except Exception as exc: 78 | logging.critical( 79 | "Unknown error occurred during public key authentication with host " 80 | f"{self.host}; Exception: {exc}" 81 | ) 82 | raise exc 83 | 84 | def _session_password_auth(self) -> None: 85 | """ 86 | Perform password based auth on SSH2NetSession 87 | 88 | Args: 89 | N/A # noqa 90 | 91 | Returns: 92 | N/A # noqa 93 | 94 | Raises: 95 | AuthenticationFailed: if authentication fails 96 | Exception: catch all for unknown other exceptions 97 | 98 | """ 99 | try: 100 | self.session.userauth_password(self.auth_user, self.auth_password) 101 | except AuthenticationError as exc: 102 | logging.critical( 103 | f"Password authentication with host {self.host} failed. Exception: {exc}." 104 | f"\n\tTrying keyboard interactive auth..." 105 | ) 106 | try: 107 | self.session.userauth_keyboardinteractive(self.auth_user, self.auth_password) 108 | except AuthenticationError as exc: 109 | logging.critical( 110 | f"Keyboard interactive authentication with host {self.host} failed. " 111 | f"Exception: {exc}." 112 | ) 113 | raise AuthenticationFailed 114 | except Exception as exc: 115 | logging.critical( 116 | "Unknown error occurred during keyboard interactive authentication with host " 117 | f"{self.host}; Exception: {exc}" 118 | ) 119 | raise exc 120 | except Exception as exc: 121 | logging.critical( 122 | "Unknown error occurred during password authentication with host " 123 | f"{self.host}; Exception: {exc}" 124 | ) 125 | raise exc 126 | 127 | def _channel_open_driver(self) -> None: 128 | """ 129 | Open channel 130 | 131 | Args: 132 | N/A # noqa 133 | 134 | Returns: 135 | N/A # noqa 136 | 137 | Raises: 138 | N/A # noqa 139 | 140 | """ 141 | self.channel = self.session.open_session() 142 | self.channel.pty() 143 | logging.debug(f"Channel to host {self.host} opened") 144 | 145 | def _channel_invoke_shell(self) -> None: 146 | """ 147 | Invoke shell on channel 148 | 149 | Args: 150 | N/A # noqa 151 | 152 | Returns: 153 | N/A # noqa 154 | 155 | Raises: 156 | N/A # noqa 157 | 158 | """ 159 | self._shell = True 160 | self.channel.shell() 161 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlmontanari/ssh2net/55e969b6d44ec3f2bd2ebbd8dedd68b99bee4c5b/tests/__init__.py -------------------------------------------------------------------------------- /tests/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlmontanari/ssh2net/55e969b6d44ec3f2bd2ebbd8dedd68b99bee4c5b/tests/functional/__init__.py -------------------------------------------------------------------------------- /tests/functional/arista_eos/expected_output/show_run: -------------------------------------------------------------------------------- 1 | ! Command: show running-config 2 | ! device: localhost (vEOS, EOS-4.22.1F) 3 | ! 4 | ! boot system flash:/vEOS-lab.swi 5 | ! 6 | transceiver qsfp default-mode 4x10G 7 | ! 8 | spanning-tree mode mstp 9 | ! 10 | no aaa root 11 | ! 12 | username vrnetlab role network-admin secret sha512 $6$LjhU1lKTyobLZJoG$PA5ZPYf8E3Xpto3BmEMG.9ItXUcsa.Bu8YPwoSX5GKlH/e.0qeP7GJh4ghTIwKySfd9GBZK1FvbtnCSy0Jlh2. 13 | ! 14 | interface Ethernet1 15 | ! 16 | interface Ethernet2 17 | ! 18 | interface Ethernet3 19 | ! 20 | interface Ethernet4 21 | ! 22 | interface Ethernet5 23 | ! 24 | interface Ethernet6 25 | ! 26 | interface Ethernet7 27 | ! 28 | interface Ethernet8 29 | ! 30 | interface Ethernet9 31 | ! 32 | interface Ethernet10 33 | ! 34 | interface Ethernet11 35 | ! 36 | interface Ethernet12 37 | ! 38 | interface Ethernet13 39 | ! 40 | interface Ethernet14 41 | ! 42 | interface Ethernet15 43 | ! 44 | interface Ethernet16 45 | ! 46 | interface Ethernet17 47 | ! 48 | interface Ethernet18 49 | ! 50 | interface Ethernet19 51 | ! 52 | interface Management1 53 | ip address 10.0.0.15/24 54 | ! 55 | no ip routing 56 | ! 57 | management api http-commands 58 | protocol unix-socket 59 | no shutdown 60 | ! 61 | end -------------------------------------------------------------------------------- /tests/functional/arista_eos/expected_output/show_run_no_strip: -------------------------------------------------------------------------------- 1 | ! Command: show running-config 2 | ! device: localhost (vEOS, EOS-4.22.1F) 3 | ! 4 | ! boot system flash:/vEOS-lab.swi 5 | ! 6 | transceiver qsfp default-mode 4x10G 7 | ! 8 | spanning-tree mode mstp 9 | ! 10 | no aaa root 11 | ! 12 | username vrnetlab role network-admin secret sha512 $6$LjhU1lKTyobLZJoG$PA5ZPYf8E3Xpto3BmEMG.9ItXUcsa.Bu8YPwoSX5GKlH/e.0qeP7GJh4ghTIwKySfd9GBZK1FvbtnCSy0Jlh2. 13 | ! 14 | interface Ethernet1 15 | ! 16 | interface Ethernet2 17 | ! 18 | interface Ethernet3 19 | ! 20 | interface Ethernet4 21 | ! 22 | interface Ethernet5 23 | ! 24 | interface Ethernet6 25 | ! 26 | interface Ethernet7 27 | ! 28 | interface Ethernet8 29 | ! 30 | interface Ethernet9 31 | ! 32 | interface Ethernet10 33 | ! 34 | interface Ethernet11 35 | ! 36 | interface Ethernet12 37 | ! 38 | interface Ethernet13 39 | ! 40 | interface Ethernet14 41 | ! 42 | interface Ethernet15 43 | ! 44 | interface Ethernet16 45 | ! 46 | interface Ethernet17 47 | ! 48 | interface Ethernet18 49 | ! 50 | interface Ethernet19 51 | ! 52 | interface Management1 53 | ip address 10.0.0.15/24 54 | ! 55 | no ip routing 56 | ! 57 | management api http-commands 58 | protocol unix-socket 59 | no shutdown 60 | ! 61 | end -------------------------------------------------------------------------------- /tests/functional/arista_eos/ext_test_funcs.py: -------------------------------------------------------------------------------- 1 | def eos_disable_paging(cls): 2 | cls.send_inputs("term length 0") 3 | -------------------------------------------------------------------------------- /tests/functional/arista_eos/test_eos.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import re 3 | 4 | import pytest 5 | 6 | from tests.functional.base_functional_tests import BaseFunctionalTest 7 | import ssh2net 8 | 9 | TEST_DEVICE = {"setup_host": "172.18.0.14", "auth_user": "vrnetlab", "auth_password": "VR-netlab9"} 10 | 11 | dummy_conn = ssh2net.IOSXEDriver(**TEST_DEVICE) 12 | PRIV_LEVELS = dummy_conn.privs 13 | 14 | 15 | class TestEOS(BaseFunctionalTest): 16 | def setup_method(self): 17 | self.platform_driver = ssh2net.EOSDriver 18 | 19 | self.device_type = Path(__file__).resolve().parts[-2] 20 | self.func_test_dir = ( 21 | f"{Path(ssh2net.__file__).parents[1]}/tests/functional/{self.device_type}/" 22 | ) 23 | self.test_device = TEST_DEVICE 24 | # ensure eos device gets into "priv_exec" mode since base image drops you into just "exec" 25 | self.test_device["comms_pre_login_handler"] = self.pre_login_handler 26 | self.disable_paging_ext_function = f"tests.functional.{self.device_type}.ext_test_funcs.{self.device_type.split('_')[1]}_disable_paging" 27 | 28 | @staticmethod 29 | def _replace_trailing_chars_running_config(input_data): 30 | execute_trailing_chars_pattern = re.compile(r"^end.*$", flags=re.M | re.I | re.S) 31 | input_data = re.sub(execute_trailing_chars_pattern, "end", input_data) 32 | return input_data 33 | 34 | @staticmethod 35 | def replace_timestamps(input_data): 36 | datetime_pattern = re.compile( 37 | r"(mon|tue|wed|thu|fri|sat|sun)\s+(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s+\d+\s+\d+:\d+:\d+\s+\d+$", 38 | flags=re.M | re.I, 39 | ) 40 | input_data = re.sub(datetime_pattern, "TIME_STAMP_REPLACED", input_data) 41 | return input_data 42 | 43 | @staticmethod 44 | def _replace_crypto_strings(input_data): 45 | crypto_pattern = re.compile(r"secret\ssha512\s[\w$\.\/]+$", flags=re.M | re.I) 46 | input_data = re.sub(crypto_pattern, "CRYPTO_REPLACED", input_data) 47 | return input_data 48 | 49 | def disable_paging(self, cls): 50 | cls.send_inputs("term length 0") 51 | 52 | def pre_login_handler(self, cls): 53 | cls.send_inputs("enable") 54 | 55 | def test_show_run_execute(self): 56 | pytest.skip("no ssh2 support for keyboard interactive auth") 57 | 58 | @pytest.mark.parametrize("setup_use_paramiko", [True], ids=["paramiko"]) 59 | def test_show_run_inputs(self, setup_use_paramiko): 60 | super().test_show_run_inputs(setup_use_paramiko) 61 | 62 | @pytest.mark.parametrize("setup_use_paramiko", [True], ids=["paramiko"]) 63 | def test_show_run_inputs_no_strip_prompt(self, setup_use_paramiko): 64 | super().test_show_run_inputs_no_strip_prompt(setup_use_paramiko) 65 | 66 | @pytest.mark.parametrize("setup_use_paramiko", [True], ids=["paramiko"]) 67 | def test_send_inputs_interact(self, setup_use_paramiko): 68 | pytest.skip("dont know what to do that is interactive on eos...?") 69 | 70 | @pytest.mark.parametrize("setup_use_paramiko", [True], ids=["paramiko"]) 71 | def test_disable_paging_function(self, setup_use_paramiko): 72 | super().test_disable_paging_function(setup_use_paramiko) 73 | 74 | @pytest.mark.parametrize("setup_use_paramiko", [True], ids=["paramiko"]) 75 | def test_disable_paging_external_function(self, setup_use_paramiko): 76 | super().test_disable_paging_external_function(setup_use_paramiko) 77 | 78 | @pytest.mark.parametrize("setup_use_paramiko", [True], ids=["paramiko"]) 79 | @pytest.mark.parametrize("priv_level", [priv for priv in PRIV_LEVELS.values()]) 80 | def test_acquire_all_priv_levels(self, setup_use_paramiko, priv_level): 81 | super().test_acquire_all_priv_levels(setup_use_paramiko, priv_level) 82 | 83 | @pytest.mark.parametrize("setup_use_paramiko", [True], ids=["paramiko"]) 84 | def test__determine_current_priv_special_configuration(self, setup_use_paramiko): 85 | with self.platform_driver( 86 | **self.test_device, setup_use_paramiko=setup_use_paramiko 87 | ) as conn: 88 | conn.send_inputs(["configure terminal", "interface Ethernet1"]) 89 | current_prompt = conn.get_prompt() 90 | current_priv = conn._determine_current_priv(current_prompt) 91 | assert current_priv.name == "special_configuration" 92 | -------------------------------------------------------------------------------- /tests/functional/base_functional_tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import ssh2net 4 | 5 | NET2_DIR = ssh2net.__file__ 6 | privs = None 7 | 8 | 9 | class BaseFunctionalTest: 10 | def setup_method(self): 11 | pass 12 | 13 | @staticmethod 14 | def _replace_trailing_chars_running_config(input_data): 15 | return input_data 16 | 17 | @staticmethod 18 | def _replace_config_bytes(input_data): 19 | return input_data 20 | 21 | @staticmethod 22 | def _replace_timestamps(input_data): 23 | return input_data 24 | 25 | @staticmethod 26 | def _replace_configured_by(input_data): 27 | return input_data 28 | 29 | @staticmethod 30 | def _replace_crypto_strings(input_data): 31 | return input_data 32 | 33 | def clean_input_data(self, input_data): 34 | input_data = self._replace_trailing_chars_running_config(input_data) 35 | input_data = self._replace_config_bytes(input_data) 36 | input_data = self._replace_timestamps(input_data) 37 | input_data = self._replace_configured_by(input_data) 38 | input_data = self._replace_crypto_strings(input_data) 39 | return input_data 40 | 41 | def disable_paging(self, cls): 42 | # implement platform specific disable paging string function here 43 | pass 44 | 45 | def show_run_execute(self): 46 | conn = ssh2net.SSH2Net(**self.test_device) 47 | show_run = conn.open_and_execute("show run") 48 | show_run = self.clean_input_data(show_run) 49 | return show_run 50 | 51 | def show_run_inputs(self, setup_use_paramiko, command): 52 | with ssh2net.SSH2Net(**self.test_device, setup_use_paramiko=setup_use_paramiko) as conn: 53 | show_run = conn.send_inputs(command)[0] 54 | show_run = self.clean_input_data(show_run) 55 | return show_run 56 | 57 | def show_run_inputs_no_strip_prompt(self, setup_use_paramiko, command): 58 | with ssh2net.SSH2Net(**self.test_device, setup_use_paramiko=setup_use_paramiko) as conn: 59 | show_run = conn.send_inputs(command, strip_prompt=False)[0] 60 | show_run = self.clean_input_data(show_run) 61 | return show_run 62 | 63 | def _send_inputs_interact(self, setup_use_paramiko, interact, **kwargs): 64 | with ssh2net.SSH2Net(**self.test_device, setup_use_paramiko=setup_use_paramiko) as conn: 65 | try: 66 | current_prompt = interact[3] 67 | except IndexError: 68 | current_prompt = conn.get_prompt() 69 | interactive = conn.send_inputs_interact( 70 | (interact[0], interact[1], interact[2], current_prompt), **kwargs 71 | )[0] 72 | return interactive 73 | 74 | def _disable_paging_function(self, setup_use_paramiko): 75 | with ssh2net.SSH2Net( 76 | **self.test_device, 77 | setup_use_paramiko=setup_use_paramiko, 78 | comms_disable_paging=self.disable_paging, 79 | ) as conn: 80 | show_run = conn.send_inputs("show run")[0] 81 | show_run = self.clean_input_data(show_run) 82 | return show_run 83 | 84 | def _disable_paging_external_function(self, setup_use_paramiko): 85 | with ssh2net.SSH2Net( 86 | **self.test_device, 87 | setup_use_paramiko=setup_use_paramiko, 88 | comms_disable_paging=self.disable_paging_ext_function, 89 | ) as conn: 90 | show_run = conn.send_inputs("show run")[0] 91 | show_run = self.clean_input_data(show_run) 92 | return show_run 93 | 94 | def test_show_run_execute(self): 95 | with open(f"{self.func_test_dir}expected_output/show_run_execute", "r") as f: 96 | expected_show_run = f.read().strip() 97 | expected_show_run = self.clean_input_data(expected_show_run) 98 | show_run = self.show_run_execute() 99 | assert len(show_run.splitlines()) == len(expected_show_run.splitlines()) 100 | assert show_run == expected_show_run 101 | 102 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 103 | def test_show_run_inputs(self, setup_use_paramiko, command="show run"): 104 | with open(f"{self.func_test_dir}expected_output/show_run", "r") as f: 105 | expected_show_run = f.read() 106 | expected_show_run = self.clean_input_data(expected_show_run) 107 | show_run = self.show_run_inputs(setup_use_paramiko, command) 108 | assert len(show_run.splitlines()) == len(expected_show_run.splitlines()) 109 | assert show_run == expected_show_run 110 | 111 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 112 | def test_show_run_inputs_no_strip_prompt(self, setup_use_paramiko, command="show run"): 113 | with open(f"{self.func_test_dir}expected_output/show_run", "r") as f: 114 | expected_show_run = f.read() 115 | expected_show_run = self.clean_input_data(expected_show_run) 116 | show_run = self.show_run_inputs(setup_use_paramiko, command) 117 | assert len(show_run.splitlines()) == len(expected_show_run.splitlines()) 118 | assert show_run == expected_show_run 119 | 120 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 121 | def test_send_inputs_interact(self, setup_use_paramiko, interact, clean=False, **kwargs): 122 | with open(f"{self.func_test_dir}expected_output/interactive", "r") as f: 123 | expected_interactive = f.read().strip() 124 | interactive = self._send_inputs_interact(setup_use_paramiko, interact, **kwargs) 125 | if clean: 126 | expected_interactive = self.clean_input_data(expected_interactive) 127 | interactive = self.clean_input_data(interactive) 128 | assert len(interactive.splitlines()) == len(expected_interactive.splitlines()) 129 | assert interactive == expected_interactive 130 | 131 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 132 | def test_disable_paging_function(self, setup_use_paramiko): 133 | with open(f"{self.func_test_dir}expected_output/show_run", "r") as f: 134 | expected_show_run = f.read() 135 | expected_show_run = self.clean_input_data(expected_show_run) 136 | show_run = self._disable_paging_function(setup_use_paramiko) 137 | assert len(show_run.splitlines()) == len(expected_show_run.splitlines()) 138 | assert show_run == expected_show_run 139 | 140 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 141 | def test_disable_paging_external_function(self, setup_use_paramiko): 142 | with open(f"{self.func_test_dir}expected_output/show_run", "r") as f: 143 | expected_show_run = f.read() 144 | expected_show_run = self.clean_input_data(expected_show_run) 145 | show_run = self._disable_paging_external_function(setup_use_paramiko) 146 | assert len(show_run.splitlines()) == len(expected_show_run.splitlines()) 147 | assert show_run == expected_show_run 148 | 149 | def test_acquire_all_priv_levels(self, setup_use_paramiko, priv_level): 150 | if not priv_level.requestable: 151 | pytest.skip(f"priv level {priv_level.name} is not requestable by ssh2net") 152 | with self.platform_driver( 153 | **self.test_device, setup_use_paramiko=setup_use_paramiko 154 | ) as conn: 155 | conn.attain_priv(priv_level.name) 156 | current_prompt = conn.get_prompt() 157 | current_priv = conn._determine_current_priv(current_prompt) 158 | assert current_priv.name == priv_level.name 159 | -------------------------------------------------------------------------------- /tests/functional/cisco_iosxe/expected_output/interactive: -------------------------------------------------------------------------------- 1 | Clear logging buffer [confirm] 2 | csr1000v# 3 | -------------------------------------------------------------------------------- /tests/functional/cisco_iosxe/expected_output/show_run: -------------------------------------------------------------------------------- 1 | Building configuration... 2 | 3 | Current configuration : 5241 bytes 4 | ! 5 | ! Last configuration change at 22:58:58 UTC Sat Aug 31 2019 6 | ! 7 | version 16.4 8 | service timestamps debug datetime msec 9 | service timestamps log datetime msec 10 | no platform punt-keepalive disable-kernel-core 11 | platform console serial 12 | ! 13 | hostname csr1000v 14 | ! 15 | boot-start-marker 16 | boot-end-marker 17 | ! 18 | ! 19 | ! 20 | no aaa new-model 21 | ! 22 | ! 23 | ! 24 | ! 25 | ! 26 | ! 27 | ! 28 | ! 29 | ! 30 | 31 | 32 | 33 | ip domain name example.com 34 | ! 35 | ! 36 | ! 37 | ! 38 | ! 39 | ! 40 | ! 41 | ! 42 | ! 43 | ! 44 | subscriber templating 45 | ! 46 | ! 47 | ! 48 | multilink bundle-name authenticated 49 | ! 50 | ! 51 | ! 52 | ! 53 | ! 54 | crypto pki trustpoint TP-self-signed-2656125433 55 | enrollment selfsigned 56 | subject-name cn=IOS-Self-Signed-Certificate-2656125433 57 | revocation-check none 58 | rsakeypair TP-self-signed-2656125433 59 | ! 60 | ! 61 | crypto pki certificate chain TP-self-signed-2656125433 62 | certificate self-signed 01 63 | 30820330 30820218 A0030201 02020101 300D0609 2A864886 F70D0101 05050030 64 | 31312F30 2D060355 04031326 494F532D 53656C66 2D536967 6E65642D 43657274 65 | 69666963 6174652D 32363536 31323534 3333301E 170D3139 30383331 32323538 66 | 34305A17 0D323030 31303130 30303030 305A3031 312F302D 06035504 03132649 67 | 4F532D53 656C662D 5369676E 65642D43 65727469 66696361 74652D32 36353631 68 | 32353433 33308201 22300D06 092A8648 86F70D01 01010500 0382010F 00308201 69 | 0A028201 0100CBF9 9AECD91C F35EE6F1 7E3AEF64 0A2F80B1 8DB4EE0F 4B43AF56 70 | 618C79D9 6BFA3C8E D2608CE9 0C1E4B14 731268BD BFA0F6AF 72B63B7F 44447292 71 | 0FFDCF06 6FF90BF8 C3D7AD93 3AA27FF7 B7698829 197F9B82 68FD469C BF947A0B 72 | 73B298CE AECFC3E1 C4552A9E BC3C475E 01B5C3E0 81B39F0A B0F421B8 C4C6A618 73 | 5F7BA56C 3665B91D 2BA5A9C7 9FBBADE8 E5B69F3A CDCE2E04 CB629792 4A169D82 74 | F37B4C61 F73AB9EC B2DBF89B 15C91292 7ADA094F 675E17F0 ED08DB7C B074EE44 75 | F70FCB60 2472F3F6 7C3F481A C8142FFD B90CE60D 7E04B776 0EDBC015 045165EC 76 | 0A0FA55F 3C1FA8F7 40BAD546 08F51A63 C0FBFF20 F9A3299B 20056285 EAD15955 77 | 13B778A3 967F0203 010001A3 53305130 0F060355 1D130101 FF040530 030101FF 78 | 301F0603 551D2304 18301680 14D2C007 8F54C7C7 0B9316B1 8BD2011B C827E4B0 79 | 0D301D06 03551D0E 04160414 D2C0078F 54C7C70B 9316B18B D2011BC8 27E4B00D 80 | 300D0609 2A864886 F70D0101 05050003 82010100 2B73DCE9 6BCD9A31 E347277C 81 | 4F68F10C B647EDEF E7CD7DD5 2F2CC495 E98AA80F 6B0F8459 AFFE2BB6 8C4150C4 82 | 84371CF6 5B7EDD11 84E9A119 BA38D21C 6CF9F228 E1C7C173 56E02FCE B355284D 83 | 9CDA6C18 E6CE34E4 296AA44F EF540ADF CDDF54AA 76CC9548 8AD69A79 433E31CD 84 | 0362D246 579BE703 8D20094B 3652774B 589FB3DD 8B70EAC5 732F87A2 D0A505F9 85 | C3E05C8A B96B7651 E04CB391 1A2736EF 2AE48BB9 3EB4C991 A61C1757 08B631DF 86 | F6928817 275B55F9 940062B9 FDCDAD72 F337D8D6 9F3904E5 7E73D7A5 4EAA7208 87 | 05119518 74B72517 5F386BB0 94942518 4E95C404 C0E79DD5 4FBEB199 6216BDDF 88 | EB27FB95 02958DEB D6045978 661C515E 09D96719 89 | quit 90 | 91 | 92 | ! 93 | ! 94 | ! 95 | ! 96 | ! 97 | ! 98 | ! 99 | license udi pid CSR1000V sn 9FKLJWM5EB0 100 | diagnostic bootup level minimal 101 | ! 102 | spanning-tree extend system-id 103 | netconf-yang cisco-odm actions ACL 104 | netconf-yang cisco-odm actions BGP 105 | netconf-yang cisco-odm actions OSPF 106 | netconf-yang cisco-odm actions Archive 107 | netconf-yang cisco-odm actions IPRoute 108 | netconf-yang cisco-odm actions EFPStats 109 | netconf-yang cisco-odm actions IPSLAStats 110 | netconf-yang cisco-odm actions Interfaces 111 | netconf-yang cisco-odm actions Environment 112 | netconf-yang cisco-odm actions FlowMonitor 113 | netconf-yang cisco-odm actions MemoryStats 114 | netconf-yang cisco-odm actions BFDNeighbors 115 | netconf-yang cisco-odm actions BridgeDomain 116 | netconf-yang cisco-odm actions CPUProcesses 117 | netconf-yang cisco-odm actions LLDPNeighbors 118 | netconf-yang cisco-odm actions VirtualService 119 | netconf-yang cisco-odm actions MemoryProcesses 120 | netconf-yang cisco-odm actions EthernetCFMStats 121 | netconf-yang cisco-odm actions MPLSLDPNeighbors 122 | netconf-yang cisco-odm actions PlatformSoftware 123 | netconf-yang cisco-odm actions MPLSStaticBinding 124 | netconf-yang cisco-odm actions MPLSForwardingTable 125 | netconf-yang 126 | ! 127 | restconf 128 | ! 129 | username vrnetlab privilege 15 password 0 VR-netlab9 130 | ! 131 | redundancy 132 | ! 133 | ! 134 | ! 135 | ! 136 | ! 137 | ! 138 | ! 139 | ! 140 | ! 141 | ! 142 | ! 143 | ! 144 | ! 145 | ! 146 | ! 147 | ! 148 | ! 149 | ! 150 | ! 151 | ! 152 | ! 153 | ! 154 | ! 155 | ! 156 | ! 157 | ! 158 | ! 159 | interface GigabitEthernet1 160 | ip address 10.0.0.15 255.255.255.0 161 | negotiation auto 162 | no mop enabled 163 | no mop sysid 164 | ! 165 | interface GigabitEthernet2 166 | no ip address 167 | shutdown 168 | negotiation auto 169 | no mop enabled 170 | no mop sysid 171 | ! 172 | interface GigabitEthernet3 173 | no ip address 174 | shutdown 175 | negotiation auto 176 | no mop enabled 177 | no mop sysid 178 | ! 179 | interface GigabitEthernet4 180 | no ip address 181 | shutdown 182 | negotiation auto 183 | no mop enabled 184 | no mop sysid 185 | ! 186 | interface GigabitEthernet5 187 | no ip address 188 | shutdown 189 | negotiation auto 190 | no mop enabled 191 | no mop sysid 192 | ! 193 | interface GigabitEthernet6 194 | no ip address 195 | shutdown 196 | negotiation auto 197 | no mop enabled 198 | no mop sysid 199 | ! 200 | interface GigabitEthernet7 201 | no ip address 202 | shutdown 203 | negotiation auto 204 | no mop enabled 205 | no mop sysid 206 | ! 207 | interface GigabitEthernet8 208 | no ip address 209 | shutdown 210 | negotiation auto 211 | no mop enabled 212 | no mop sysid 213 | ! 214 | interface GigabitEthernet9 215 | no ip address 216 | shutdown 217 | negotiation auto 218 | no mop enabled 219 | no mop sysid 220 | ! 221 | interface GigabitEthernet10 222 | no ip address 223 | shutdown 224 | negotiation auto 225 | no mop enabled 226 | no mop sysid 227 | ! 228 | ! 229 | virtual-service csr_mgmt 230 | ! 231 | ip forward-protocol nd 232 | no ip http server 233 | no ip http secure-server 234 | ! 235 | ! 236 | ! 237 | ! 238 | ! 239 | ! 240 | ! 241 | control-plane 242 | ! 243 | ! 244 | ! 245 | ! 246 | ! 247 | ! 248 | ! 249 | ! 250 | ! 251 | ! 252 | line con 0 253 | stopbits 1 254 | line vty 0 255 | login local 256 | transport input all 257 | line vty 1 258 | login local 259 | length 0 260 | transport input all 261 | line vty 2 4 262 | login local 263 | transport input all 264 | ! 265 | ! 266 | ! 267 | ! 268 | ! 269 | ! 270 | end 271 | -------------------------------------------------------------------------------- /tests/functional/cisco_iosxe/expected_output/show_run_execute: -------------------------------------------------------------------------------- 1 | Building configuration... 2 | 3 | Current configuration : CONFIG_BYTES 4 | ! 5 | ! Last configuration change at TIME_STAMP_REPLACED 6 | ! 7 | version 16.4 8 | service timestamps debug datetime msec 9 | service timestamps log datetime msec 10 | no platform punt-keepalive disable-kernel-core 11 | platform console serial 12 | ! 13 | hostname csr1000v 14 | ! 15 | boot-start-marker 16 | boot-end-marker 17 | ! 18 | ! 19 | ! 20 | no aaa new-model 21 | ! 22 | ! 23 | ! 24 | ! 25 | ! 26 | ! 27 | ! 28 | ! 29 | ! 30 | 31 | 32 | 33 | ip domain name example.com 34 | ! 35 | ! 36 | ! 37 | ! 38 | ! 39 | ! 40 | ! 41 | ! 42 | ! 43 | ! 44 | subscriber templating 45 | ! 46 | ! 47 | ! 48 | multilink bundle-name authenticated 49 | ! 50 | ! 51 | ! 52 | ! 53 | ! 54 | crypto pki trustpoint TP-self-signed-2656125433 55 | enrollment selfsigned 56 | subject-name cn=IOS-Self-Signed-Certificate-2656125433 57 | revocation-check none 58 | rsakeypair TP-self-signed-2656125433 59 | ! 60 | ! 61 | crypto pki certificate chain TP-self-signed-2656125433 62 | CRYPTO_REPLACED 63 | 64 | 65 | ! 66 | ! 67 | ! 68 | ! 69 | ! 70 | ! 71 | ! 72 | license udi pid CSR1000V sn 9FKLJWM5EB0 73 | diagnostic bootup level minimal 74 | ! 75 | spanning-tree extend system-id 76 | netconf-yang cisco-odm actions ACL 77 | netconf-yang cisco-odm actions BGP 78 | netconf-yang cisco-odm actions OSPF 79 | netconf-yang cisco-odm actions Archive 80 | netconf-yang cisco-odm actions IPRoute 81 | netconf-yang cisco-odm actions EFPStats 82 | netconf-yang cisco-odm actions IPSLAStats 83 | netconf-yang cisco-odm actions Interfaces 84 | netconf-yang cisco-odm actions Environment 85 | netconf-yang cisco-odm actions FlowMonitor 86 | netconf-yang cisco-odm actions MemoryStats 87 | netconf-yang cisco-odm actions BFDNeighbors 88 | netconf-yang cisco-odm actions BridgeDomain 89 | netconf-yang cisco-odm actions CPUProcesses 90 | netconf-yang cisco-odm actions LLDPNeighbors 91 | netconf-yang cisco-odm actions VirtualService 92 | netconf-yang cisco-odm actions MemoryProcesses 93 | netconf-yang cisco-odm actions EthernetCFMStats 94 | netconf-yang cisco-odm actions MPLSLDPNeighbors 95 | netconf-yang cisco-odm actions PlatformSoftware 96 | netconf-yang cisco-odm actions MPLSStaticBinding 97 | netconf-yang cisco-odm actions MPLSForwardingTable 98 | netconf-yang 99 | ! 100 | restconf 101 | ! 102 | username vrnetlab privilege 15 password 0 VR-netlab9 103 | ! 104 | redundancy 105 | ! 106 | ! 107 | ! 108 | ! 109 | ! 110 | ! 111 | ! 112 | ! 113 | ! 114 | ! 115 | ! 116 | ! 117 | ! 118 | ! 119 | ! 120 | ! 121 | ! 122 | ! 123 | ! 124 | ! 125 | ! 126 | ! 127 | ! 128 | ! 129 | ! 130 | ! 131 | ! 132 | interface GigabitEthernet1 133 | ip address 10.0.0.15 255.255.255.0 134 | negotiation auto 135 | no mop enabled 136 | no mop sysid 137 | ! 138 | interface GigabitEthernet2 139 | no ip address 140 | shutdown 141 | negotiation auto 142 | no mop enabled 143 | no mop sysid 144 | ! 145 | interface GigabitEthernet3 146 | no ip address 147 | shutdown 148 | negotiation auto 149 | no mop enabled 150 | no mop sysid 151 | ! 152 | interface GigabitEthernet4 153 | no ip address 154 | shutdown 155 | negotiation auto 156 | no mop enabled 157 | no mop sysid 158 | ! 159 | interface GigabitEthernet5 160 | no ip address 161 | shutdown 162 | negotiation auto 163 | no mop enabled 164 | no mop sysid 165 | ! 166 | interface GigabitEthernet6 167 | no ip address 168 | shutdown 169 | negotiation auto 170 | no mop enabled 171 | no mop sysid 172 | ! 173 | interface GigabitEthernet7 174 | no ip address 175 | shutdown 176 | negotiation auto 177 | no mop enabled 178 | no mop sysid 179 | ! 180 | interface GigabitEthernet8 181 | no ip address 182 | shutdown 183 | negotiation auto 184 | no mop enabled 185 | no mop sysid 186 | ! 187 | interface GigabitEthernet9 188 | no ip address 189 | shutdown 190 | negotiation auto 191 | no mop enabled 192 | no mop sysid 193 | ! 194 | interface GigabitEthernet10 195 | no ip address 196 | shutdown 197 | negotiation auto 198 | no mop enabled 199 | no mop sysid 200 | ! 201 | ! 202 | virtual-service csr_mgmt 203 | ! 204 | ip forward-protocol nd 205 | no ip http server 206 | no ip http secure-server 207 | ! 208 | ! 209 | ! 210 | ! 211 | ! 212 | ! 213 | ! 214 | control-plane 215 | ! 216 | ! 217 | ! 218 | ! 219 | ! 220 | ! 221 | ! 222 | ! 223 | ! 224 | ! 225 | line con 0 226 | stopbits 1 227 | line vty 0 228 | login local 229 | transport input all 230 | line vty 1 231 | login local 232 | length 0 233 | transport input all 234 | line vty 2 4 235 | login local 236 | transport input all 237 | ! 238 | ! 239 | ! 240 | ! 241 | ! 242 | ! 243 | end 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | csr1000v tty3 is now available 307 | 308 | 309 | 310 | 311 | 312 | Press RETURN to get started. -------------------------------------------------------------------------------- /tests/functional/cisco_iosxe/expected_output/show_run_no_strip: -------------------------------------------------------------------------------- 1 | Building configuration... 2 | 3 | Current configuration : 5241 bytes 4 | ! 5 | ! Last configuration change at 22:58:58 UTC Sat Aug 31 2019 6 | ! 7 | version 16.4 8 | service timestamps debug datetime msec 9 | service timestamps log datetime msec 10 | no platform punt-keepalive disable-kernel-core 11 | platform console serial 12 | ! 13 | hostname csr1000v 14 | ! 15 | boot-start-marker 16 | boot-end-marker 17 | ! 18 | ! 19 | ! 20 | no aaa new-model 21 | ! 22 | ! 23 | ! 24 | ! 25 | ! 26 | ! 27 | ! 28 | ! 29 | ! 30 | 31 | 32 | 33 | ip domain name example.com 34 | ! 35 | ! 36 | ! 37 | ! 38 | ! 39 | ! 40 | ! 41 | ! 42 | ! 43 | ! 44 | subscriber templating 45 | ! 46 | ! 47 | ! 48 | multilink bundle-name authenticated 49 | ! 50 | ! 51 | ! 52 | ! 53 | ! 54 | crypto pki trustpoint TP-self-signed-2656125433 55 | enrollment selfsigned 56 | subject-name cn=IOS-Self-Signed-Certificate-2656125433 57 | revocation-check none 58 | rsakeypair TP-self-signed-2656125433 59 | ! 60 | ! 61 | crypto pki certificate chain TP-self-signed-2656125433 62 | certificate self-signed 01 63 | 30820330 30820218 A0030201 02020101 300D0609 2A864886 F70D0101 05050030 64 | 31312F30 2D060355 04031326 494F532D 53656C66 2D536967 6E65642D 43657274 65 | 69666963 6174652D 32363536 31323534 3333301E 170D3139 30383331 32323538 66 | 34305A17 0D323030 31303130 30303030 305A3031 312F302D 06035504 03132649 67 | 4F532D53 656C662D 5369676E 65642D43 65727469 66696361 74652D32 36353631 68 | 32353433 33308201 22300D06 092A8648 86F70D01 01010500 0382010F 00308201 69 | 0A028201 0100CBF9 9AECD91C F35EE6F1 7E3AEF64 0A2F80B1 8DB4EE0F 4B43AF56 70 | 618C79D9 6BFA3C8E D2608CE9 0C1E4B14 731268BD BFA0F6AF 72B63B7F 44447292 71 | 0FFDCF06 6FF90BF8 C3D7AD93 3AA27FF7 B7698829 197F9B82 68FD469C BF947A0B 72 | 73B298CE AECFC3E1 C4552A9E BC3C475E 01B5C3E0 81B39F0A B0F421B8 C4C6A618 73 | 5F7BA56C 3665B91D 2BA5A9C7 9FBBADE8 E5B69F3A CDCE2E04 CB629792 4A169D82 74 | F37B4C61 F73AB9EC B2DBF89B 15C91292 7ADA094F 675E17F0 ED08DB7C B074EE44 75 | F70FCB60 2472F3F6 7C3F481A C8142FFD B90CE60D 7E04B776 0EDBC015 045165EC 76 | 0A0FA55F 3C1FA8F7 40BAD546 08F51A63 C0FBFF20 F9A3299B 20056285 EAD15955 77 | 13B778A3 967F0203 010001A3 53305130 0F060355 1D130101 FF040530 030101FF 78 | 301F0603 551D2304 18301680 14D2C007 8F54C7C7 0B9316B1 8BD2011B C827E4B0 79 | 0D301D06 03551D0E 04160414 D2C0078F 54C7C70B 9316B18B D2011BC8 27E4B00D 80 | 300D0609 2A864886 F70D0101 05050003 82010100 2B73DCE9 6BCD9A31 E347277C 81 | 4F68F10C B647EDEF E7CD7DD5 2F2CC495 E98AA80F 6B0F8459 AFFE2BB6 8C4150C4 82 | 84371CF6 5B7EDD11 84E9A119 BA38D21C 6CF9F228 E1C7C173 56E02FCE B355284D 83 | 9CDA6C18 E6CE34E4 296AA44F EF540ADF CDDF54AA 76CC9548 8AD69A79 433E31CD 84 | 0362D246 579BE703 8D20094B 3652774B 589FB3DD 8B70EAC5 732F87A2 D0A505F9 85 | C3E05C8A B96B7651 E04CB391 1A2736EF 2AE48BB9 3EB4C991 A61C1757 08B631DF 86 | F6928817 275B55F9 940062B9 FDCDAD72 F337D8D6 9F3904E5 7E73D7A5 4EAA7208 87 | 05119518 74B72517 5F386BB0 94942518 4E95C404 C0E79DD5 4FBEB199 6216BDDF 88 | EB27FB95 02958DEB D6045978 661C515E 09D96719 89 | quit 90 | 91 | 92 | ! 93 | ! 94 | ! 95 | ! 96 | ! 97 | ! 98 | ! 99 | license udi pid CSR1000V sn 9FKLJWM5EB0 100 | diagnostic bootup level minimal 101 | ! 102 | spanning-tree extend system-id 103 | netconf-yang cisco-odm actions ACL 104 | netconf-yang cisco-odm actions BGP 105 | netconf-yang cisco-odm actions OSPF 106 | netconf-yang cisco-odm actions Archive 107 | netconf-yang cisco-odm actions IPRoute 108 | netconf-yang cisco-odm actions EFPStats 109 | netconf-yang cisco-odm actions IPSLAStats 110 | netconf-yang cisco-odm actions Interfaces 111 | netconf-yang cisco-odm actions Environment 112 | netconf-yang cisco-odm actions FlowMonitor 113 | netconf-yang cisco-odm actions MemoryStats 114 | netconf-yang cisco-odm actions BFDNeighbors 115 | netconf-yang cisco-odm actions BridgeDomain 116 | netconf-yang cisco-odm actions CPUProcesses 117 | netconf-yang cisco-odm actions LLDPNeighbors 118 | netconf-yang cisco-odm actions VirtualService 119 | netconf-yang cisco-odm actions MemoryProcesses 120 | netconf-yang cisco-odm actions EthernetCFMStats 121 | netconf-yang cisco-odm actions MPLSLDPNeighbors 122 | netconf-yang cisco-odm actions PlatformSoftware 123 | netconf-yang cisco-odm actions MPLSStaticBinding 124 | netconf-yang cisco-odm actions MPLSForwardingTable 125 | netconf-yang 126 | ! 127 | restconf 128 | ! 129 | username vrnetlab privilege 15 password 0 VR-netlab9 130 | ! 131 | redundancy 132 | ! 133 | ! 134 | ! 135 | ! 136 | ! 137 | ! 138 | ! 139 | ! 140 | ! 141 | ! 142 | ! 143 | ! 144 | ! 145 | ! 146 | ! 147 | ! 148 | ! 149 | ! 150 | ! 151 | ! 152 | ! 153 | ! 154 | ! 155 | ! 156 | ! 157 | ! 158 | ! 159 | interface GigabitEthernet1 160 | ip address 10.0.0.15 255.255.255.0 161 | negotiation auto 162 | no mop enabled 163 | no mop sysid 164 | ! 165 | interface GigabitEthernet2 166 | no ip address 167 | shutdown 168 | negotiation auto 169 | no mop enabled 170 | no mop sysid 171 | ! 172 | interface GigabitEthernet3 173 | no ip address 174 | shutdown 175 | negotiation auto 176 | no mop enabled 177 | no mop sysid 178 | ! 179 | interface GigabitEthernet4 180 | no ip address 181 | shutdown 182 | negotiation auto 183 | no mop enabled 184 | no mop sysid 185 | ! 186 | interface GigabitEthernet5 187 | no ip address 188 | shutdown 189 | negotiation auto 190 | no mop enabled 191 | no mop sysid 192 | ! 193 | interface GigabitEthernet6 194 | no ip address 195 | shutdown 196 | negotiation auto 197 | no mop enabled 198 | no mop sysid 199 | ! 200 | interface GigabitEthernet7 201 | no ip address 202 | shutdown 203 | negotiation auto 204 | no mop enabled 205 | no mop sysid 206 | ! 207 | interface GigabitEthernet8 208 | no ip address 209 | shutdown 210 | negotiation auto 211 | no mop enabled 212 | no mop sysid 213 | ! 214 | interface GigabitEthernet9 215 | no ip address 216 | shutdown 217 | negotiation auto 218 | no mop enabled 219 | no mop sysid 220 | ! 221 | interface GigabitEthernet10 222 | no ip address 223 | shutdown 224 | negotiation auto 225 | no mop enabled 226 | no mop sysid 227 | ! 228 | ! 229 | virtual-service csr_mgmt 230 | ! 231 | ip forward-protocol nd 232 | no ip http server 233 | no ip http secure-server 234 | ! 235 | ! 236 | ! 237 | ! 238 | ! 239 | ! 240 | ! 241 | control-plane 242 | ! 243 | ! 244 | ! 245 | ! 246 | ! 247 | ! 248 | ! 249 | ! 250 | ! 251 | ! 252 | line con 0 253 | stopbits 1 254 | line vty 0 255 | login local 256 | transport input all 257 | line vty 1 258 | login local 259 | length 0 260 | transport input all 261 | line vty 2 4 262 | login local 263 | transport input all 264 | ! 265 | ! 266 | ! 267 | ! 268 | ! 269 | ! 270 | end 271 | 272 | csr1000v# 273 | 274 | -------------------------------------------------------------------------------- /tests/functional/cisco_iosxe/ext_test_funcs.py: -------------------------------------------------------------------------------- 1 | def iosxe_disable_paging(cls): 2 | cls.send_inputs("term length 0") 3 | -------------------------------------------------------------------------------- /tests/functional/cisco_iosxe/test_iosxe.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import re 3 | 4 | import pytest 5 | 6 | from tests.functional.base_functional_tests import BaseFunctionalTest 7 | import ssh2net 8 | 9 | TEST_DEVICE = {"setup_host": "172.18.0.11", "auth_user": "vrnetlab", "auth_password": "VR-netlab9"} 10 | 11 | dummy_conn = ssh2net.IOSXEDriver(**TEST_DEVICE) 12 | PRIV_LEVELS = dummy_conn.privs 13 | 14 | 15 | class TestIOSXE(BaseFunctionalTest): 16 | def setup_method(self): 17 | self.platform_driver = ssh2net.IOSXEDriver 18 | 19 | self.device_type = Path(__file__).resolve().parts[-2] 20 | self.func_test_dir = ( 21 | f"{Path(ssh2net.__file__).parents[1]}/tests/functional/{self.device_type}/" 22 | ) 23 | self.test_device = TEST_DEVICE 24 | self.disable_paging_ext_function = f"tests.functional.{self.device_type}.ext_test_funcs.{self.device_type.split('_')[1]}_disable_paging" 25 | 26 | @staticmethod 27 | def _replace_trailing_chars_running_config(input_data): 28 | execute_trailing_chars_pattern = re.compile(r"^end.*$", flags=re.M | re.I | re.S) 29 | input_data = re.sub(execute_trailing_chars_pattern, "end", input_data) 30 | return input_data 31 | 32 | @staticmethod 33 | def _replace_config_bytes(input_data): 34 | config_bytes_pattern = re.compile(r"^Current configuration : \d+ bytes$", flags=re.M | re.I) 35 | input_data = re.sub( 36 | config_bytes_pattern, "Current configuration : CONFIG_BYTES", input_data 37 | ) 38 | return input_data 39 | 40 | @staticmethod 41 | def _replace_timestamps(input_data): 42 | datetime_pattern = re.compile( 43 | r"\d+:\d+:\d+\d+\s+[a-z]{3}\s+(mon|tue|wed|thu|fri|sat|sun)\s+(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s+\d+\s+\d+", 44 | flags=re.M | re.I, 45 | ) 46 | input_data = re.sub(datetime_pattern, "TIME_STAMP_REPLACED", input_data) 47 | return input_data 48 | 49 | @staticmethod 50 | def _replace_configured_by(input_data): 51 | configured_by_pattern = re.compile( 52 | r"^! Last configuration change at TIME_STAMP_REPLACED by (\w+)$", flags=re.M | re.I 53 | ) 54 | input_data = re.sub( 55 | configured_by_pattern, "! Last configuration change at TIME_STAMP_REPLACED", input_data 56 | ) 57 | return input_data 58 | 59 | @staticmethod 60 | def _replace_crypto_strings(input_data): 61 | crypto_pattern = re.compile( 62 | r"^\s+certificate self-signed.*$\s(^\s{2}(\w+\s){1,8})+\s+quit$", flags=re.M | re.I 63 | ) 64 | input_data = re.sub(crypto_pattern, "CRYPTO_REPLACED", input_data) 65 | return input_data 66 | 67 | def disable_paging(self, cls): 68 | cls.send_inputs("term length 0") 69 | 70 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 71 | def test_send_inputs_interact(self, setup_use_paramiko): 72 | interact = ["clear logg", "Clear logging buffer [confirm]", ""] 73 | super().test_send_inputs_interact(setup_use_paramiko, interact) 74 | 75 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 76 | @pytest.mark.parametrize("priv_level", [priv for priv in PRIV_LEVELS.values()]) 77 | def test_acquire_all_priv_levels(self, setup_use_paramiko, priv_level): 78 | super().test_acquire_all_priv_levels(setup_use_paramiko, priv_level) 79 | 80 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 81 | def test__determine_current_priv_special_configuration(self, setup_use_paramiko): 82 | with self.platform_driver( 83 | **self.test_device, setup_use_paramiko=setup_use_paramiko 84 | ) as conn: 85 | conn.send_inputs(["configure terminal", "interface GigabitEthernet1"]) 86 | current_prompt = conn.get_prompt() 87 | current_priv = conn._determine_current_priv(current_prompt) 88 | assert current_priv.name == "special_configuration" 89 | -------------------------------------------------------------------------------- /tests/functional/cisco_iosxr/expected_output/interactive: -------------------------------------------------------------------------------- 1 | Fri Sep 6 01:06:15.228 UTC 2 | Clear logging buffer [confirm] [y/n] :y 3 | RP/0/RP0/CPU0:ios# 4 | -------------------------------------------------------------------------------- /tests/functional/cisco_iosxr/ext_test_funcs.py: -------------------------------------------------------------------------------- 1 | def iosxr_disable_paging(cls): 2 | cls.send_inputs("term length 0") 3 | -------------------------------------------------------------------------------- /tests/functional/cisco_iosxr/test_iosxr.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import re 3 | 4 | import pytest 5 | 6 | from tests.functional.base_functional_tests import BaseFunctionalTest 7 | from ssh2net.core.cisco_iosxr.driver import comms_pre_login_handler 8 | import ssh2net 9 | 10 | TEST_DEVICE = {"setup_host": "172.18.0.13", "auth_user": "vrnetlab", "auth_password": "VR-netlab9"} 11 | 12 | dummy_conn = ssh2net.IOSXRDriver(**TEST_DEVICE) 13 | PRIV_LEVELS = dummy_conn.privs 14 | PRE_LOGIN_HANDLER = comms_pre_login_handler 15 | 16 | 17 | class TestIOSXR(BaseFunctionalTest): 18 | def setup_method(self): 19 | self.platform_driver = ssh2net.IOSXRDriver 20 | 21 | self.device_type = Path(__file__).resolve().parts[-2] 22 | self.func_test_dir = ( 23 | f"{Path(ssh2net.__file__).parents[1]}/tests/functional/{self.device_type}/" 24 | ) 25 | test_device = TEST_DEVICE.copy() 26 | test_device["comms_pre_login_handler"] = PRE_LOGIN_HANDLER 27 | self.test_device = test_device 28 | self.disable_paging_ext_function = f"tests.functional.{self.device_type}.ext_test_funcs.{self.device_type.split('_')[1]}_disable_paging" 29 | 30 | @staticmethod 31 | def _replace_trailing_chars_running_config(input_data): 32 | execute_trailing_chars_pattern = re.compile(r"^end.*$", flags=re.M | re.I | re.S) 33 | input_data = re.sub(execute_trailing_chars_pattern, "end", input_data) 34 | return input_data 35 | 36 | @staticmethod 37 | def _replace_timestamps(input_data): 38 | datetime_pattern = re.compile( 39 | r"(mon|tue|wed|thu|fri|sat|sun)\s+(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s+\d+\s+\d+:\d+:\d+((\.\d+\s\w+)|\s\d+)", 40 | flags=re.M | re.I, 41 | ) 42 | input_data = re.sub(datetime_pattern, "TIME_STAMP_REPLACED", input_data) 43 | return input_data 44 | 45 | @staticmethod 46 | def _replace_configured_by(input_data): 47 | configured_by_pattern = re.compile( 48 | r"^!! Last configuration change at TIME_STAMP_REPLACED by (\w+)$", flags=re.M | re.I 49 | ) 50 | input_data = re.sub( 51 | configured_by_pattern, "!! Last configuration change at TIME_STAMP_REPLACED", input_data 52 | ) 53 | return input_data 54 | 55 | @staticmethod 56 | def _replace_crypto_strings(input_data): 57 | crypto_pattern = re.compile(r"^\ssecret\s5\s[\w$\.\/]+$", flags=re.M | re.I) 58 | input_data = re.sub(crypto_pattern, "CRYPTO_REPLACED", input_data) 59 | return input_data 60 | 61 | def disable_paging(self, cls): 62 | cls.send_inputs("term length 0") 63 | 64 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 65 | def test_send_inputs_interact(self, setup_use_paramiko): 66 | interact = ["clear logg", "Clear logging buffer [confirm] [y/n] :", "y"] 67 | super().test_send_inputs_interact(setup_use_paramiko, interact, clean=True) 68 | 69 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 70 | @pytest.mark.parametrize("priv_level", [priv for priv in PRIV_LEVELS.values()]) 71 | def test_acquire_all_priv_levels(self, setup_use_paramiko, priv_level): 72 | super().test_acquire_all_priv_levels(setup_use_paramiko, priv_level) 73 | 74 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 75 | def test__determine_current_priv_special_configuration(self, setup_use_paramiko): 76 | with self.platform_driver( 77 | **self.test_device, setup_use_paramiko=setup_use_paramiko 78 | ) as conn: 79 | conn.send_inputs(["configure terminal", "interface GigabitEthernet0/0/0/127"]) 80 | current_prompt = conn.get_prompt() 81 | current_priv = conn._determine_current_priv(current_prompt) 82 | assert current_priv.name == "special_configuration" 83 | -------------------------------------------------------------------------------- /tests/functional/cisco_nxos/expected_output/interactive: -------------------------------------------------------------------------------- 1 | Do you want to delete "/virtual-instance.conf" ? (yes/no/abort) [y]n 2 | 3 | 4 | switch# 5 | -------------------------------------------------------------------------------- /tests/functional/cisco_nxos/expected_output/show_run: -------------------------------------------------------------------------------- 1 | !Command: show running-config 2 | !Running configuration last done at: Sun Sep 1 02:24:31 2019 3 | !Time: Sun Sep 1 02:27:46 2019 4 | 5 | version 9.2(4) Bios:version 6 | vdc switch id 1 7 | limit-resource vlan minimum 16 maximum 4094 8 | limit-resource vrf minimum 2 maximum 4096 9 | limit-resource port-channel minimum 0 maximum 511 10 | limit-resource u4route-mem minimum 128 maximum 128 11 | limit-resource u6route-mem minimum 96 maximum 96 12 | limit-resource m4route-mem minimum 58 maximum 58 13 | limit-resource m6route-mem minimum 8 maximum 8 14 | no password strength-check 15 | username admin password 5 $5$DICBLF$Xhpg8rM1hb46a98tsxe249e9m6oa3TH00Y8C40.8JDC role network-admin 16 | username vrnetlab password 5 $5$tGG3FFLY$YXOnmUXhkQE5Vn6KCOzpZBABLFvEzochGeH87YOh2q7 role network-admin 17 | username vrnetlab passphrase lifetime 99999 warntime 14 gracetime 3 18 | ip domain-lookup 19 | copp profile strict 20 | snmp-server user admin network-admin auth md5 0x2d0b9ec17b5d1c3bc718bb33c08528a1 priv 0x2d0b9ec17b5d1c3bc718bb33c08528a1 localizedkey 21 | snmp-server user vrnetlab network-admin auth md5 0x3b9301da0b9f328bc2573fed20773dcb priv 0x3b9301da0b9f328bc2573fed20773dcb localizedkey 22 | rmon event 1 description FATAL(1) owner PMON@FATAL 23 | rmon event 2 description CRITICAL(2) owner PMON@CRITICAL 24 | rmon event 3 description ERROR(3) owner PMON@ERROR 25 | rmon event 4 description WARNING(4) owner PMON@WARNING 26 | rmon event 5 description INFORMATION(5) owner PMON@INFO 27 | 28 | vlan 1 29 | 30 | vrf context management 31 | 32 | interface Ethernet1/1 33 | 34 | interface Ethernet1/2 35 | 36 | interface Ethernet1/3 37 | 38 | interface Ethernet1/4 39 | 40 | interface Ethernet1/5 41 | 42 | interface Ethernet1/6 43 | 44 | interface Ethernet1/7 45 | 46 | interface Ethernet1/8 47 | 48 | interface Ethernet1/9 49 | 50 | interface Ethernet1/10 51 | 52 | interface Ethernet1/11 53 | 54 | interface Ethernet1/12 55 | 56 | interface Ethernet1/13 57 | 58 | interface Ethernet1/14 59 | 60 | interface Ethernet1/15 61 | 62 | interface Ethernet1/16 63 | 64 | interface Ethernet1/17 65 | 66 | interface Ethernet1/18 67 | 68 | interface Ethernet1/19 69 | 70 | interface Ethernet1/20 71 | 72 | interface Ethernet1/21 73 | 74 | interface Ethernet1/22 75 | 76 | interface Ethernet1/23 77 | 78 | interface Ethernet1/24 79 | 80 | interface Ethernet1/25 81 | 82 | interface Ethernet1/26 83 | 84 | interface Ethernet1/27 85 | 86 | interface Ethernet1/28 87 | 88 | interface Ethernet1/29 89 | 90 | interface Ethernet1/30 91 | 92 | interface Ethernet1/31 93 | 94 | interface Ethernet1/32 95 | 96 | interface Ethernet1/33 97 | 98 | interface Ethernet1/34 99 | 100 | interface Ethernet1/35 101 | 102 | interface Ethernet1/36 103 | 104 | interface Ethernet1/37 105 | 106 | interface Ethernet1/38 107 | 108 | interface Ethernet1/39 109 | 110 | interface Ethernet1/40 111 | 112 | interface Ethernet1/41 113 | 114 | interface Ethernet1/42 115 | 116 | interface Ethernet1/43 117 | 118 | interface Ethernet1/44 119 | 120 | interface Ethernet1/45 121 | 122 | interface Ethernet1/46 123 | 124 | interface Ethernet1/47 125 | 126 | interface Ethernet1/48 127 | 128 | interface Ethernet1/49 129 | 130 | interface Ethernet1/50 131 | 132 | interface Ethernet1/51 133 | 134 | interface Ethernet1/52 135 | 136 | interface Ethernet1/53 137 | 138 | interface Ethernet1/54 139 | 140 | interface Ethernet1/55 141 | 142 | interface Ethernet1/56 143 | 144 | interface Ethernet1/57 145 | 146 | interface Ethernet1/58 147 | 148 | interface Ethernet1/59 149 | 150 | interface Ethernet1/60 151 | 152 | interface Ethernet1/61 153 | 154 | interface Ethernet1/62 155 | 156 | interface Ethernet1/63 157 | 158 | interface Ethernet1/64 159 | 160 | interface Ethernet1/65 161 | 162 | interface Ethernet1/66 163 | 164 | interface Ethernet1/67 165 | 166 | interface Ethernet1/68 167 | 168 | interface Ethernet1/69 169 | 170 | interface Ethernet1/70 171 | 172 | interface Ethernet1/71 173 | 174 | interface Ethernet1/72 175 | 176 | interface Ethernet1/73 177 | 178 | interface Ethernet1/74 179 | 180 | interface Ethernet1/75 181 | 182 | interface Ethernet1/76 183 | 184 | interface Ethernet1/77 185 | 186 | interface Ethernet1/78 187 | 188 | interface Ethernet1/79 189 | 190 | interface Ethernet1/80 191 | 192 | interface Ethernet1/81 193 | 194 | interface Ethernet1/82 195 | 196 | interface Ethernet1/83 197 | 198 | interface Ethernet1/84 199 | 200 | interface Ethernet1/85 201 | 202 | interface Ethernet1/86 203 | 204 | interface Ethernet1/87 205 | 206 | interface Ethernet1/88 207 | 208 | interface Ethernet1/89 209 | 210 | interface Ethernet1/90 211 | 212 | interface Ethernet1/91 213 | 214 | interface Ethernet1/92 215 | 216 | interface Ethernet1/93 217 | 218 | interface Ethernet1/94 219 | 220 | interface Ethernet1/95 221 | 222 | interface Ethernet1/96 223 | 224 | interface Ethernet1/97 225 | 226 | interface Ethernet1/98 227 | 228 | interface Ethernet1/99 229 | 230 | interface Ethernet1/100 231 | 232 | interface Ethernet1/101 233 | 234 | interface Ethernet1/102 235 | 236 | interface Ethernet1/103 237 | 238 | interface Ethernet1/104 239 | 240 | interface Ethernet1/105 241 | 242 | interface Ethernet1/106 243 | 244 | interface Ethernet1/107 245 | 246 | interface Ethernet1/108 247 | 248 | interface Ethernet1/109 249 | 250 | interface Ethernet1/110 251 | 252 | interface Ethernet1/111 253 | 254 | interface Ethernet1/112 255 | 256 | interface Ethernet1/113 257 | 258 | interface Ethernet1/114 259 | 260 | interface Ethernet1/115 261 | 262 | interface Ethernet1/116 263 | 264 | interface Ethernet1/117 265 | 266 | interface Ethernet1/118 267 | 268 | interface Ethernet1/119 269 | 270 | interface Ethernet1/120 271 | 272 | interface Ethernet1/121 273 | 274 | interface Ethernet1/122 275 | 276 | interface Ethernet1/123 277 | 278 | interface Ethernet1/124 279 | 280 | interface Ethernet1/125 281 | 282 | interface Ethernet1/126 283 | 284 | interface Ethernet1/127 285 | 286 | interface Ethernet1/128 287 | 288 | interface mgmt0 289 | vrf member management 290 | ip address 10.0.0.15/24 291 | line console 292 | line vty 293 | -------------------------------------------------------------------------------- /tests/functional/cisco_nxos/expected_output/show_run_execute: -------------------------------------------------------------------------------- 1 | !Command: show running-config 2 | !Running configuration last done at: Sun Sep 1 02:24:31 2019 3 | !Time: Sun Sep 1 02:27:46 2019 4 | 5 | version 9.2(4) Bios:version 6 | vdc switch id 1 7 | limit-resource vlan minimum 16 maximum 4094 8 | limit-resource vrf minimum 2 maximum 4096 9 | limit-resource port-channel minimum 0 maximum 511 10 | limit-resource u4route-mem minimum 128 maximum 128 11 | limit-resource u6route-mem minimum 96 maximum 96 12 | limit-resource m4route-mem minimum 58 maximum 58 13 | limit-resource m6route-mem minimum 8 maximum 8 14 | no password strength-check 15 | username admin password 5 $5$DICBLF$Xhpg8rM1hb46a98tsxe249e9m6oa3TH00Y8C40.8JDC role network-admin 16 | username vrnetlab password 5 $5$tGG3FFLY$YXOnmUXhkQE5Vn6KCOzpZBABLFvEzochGeH87YOh2q7 role network-admin 17 | username vrnetlab passphrase lifetime 99999 warntime 14 gracetime 3 18 | ip domain-lookup 19 | copp profile strict 20 | snmp-server user admin network-admin auth md5 0x2d0b9ec17b5d1c3bc718bb33c08528a1 priv 0x2d0b9ec17b5d1c3bc718bb33c08528a1 localizedkey 21 | snmp-server user vrnetlab network-admin auth md5 0x3b9301da0b9f328bc2573fed20773dcb priv 0x3b9301da0b9f328bc2573fed20773dcb localizedkey 22 | rmon event 1 description FATAL(1) owner PMON@FATAL 23 | rmon event 2 description CRITICAL(2) owner PMON@CRITICAL 24 | rmon event 3 description ERROR(3) owner PMON@ERROR 25 | rmon event 4 description WARNING(4) owner PMON@WARNING 26 | rmon event 5 description INFORMATION(5) owner PMON@INFO 27 | 28 | vlan 1 29 | 30 | vrf context management 31 | 32 | interface Ethernet1/1 33 | 34 | interface Ethernet1/2 35 | 36 | interface Ethernet1/3 37 | 38 | interface Ethernet1/4 39 | 40 | interface Ethernet1/5 41 | 42 | interface Ethernet1/6 43 | 44 | interface Ethernet1/7 45 | 46 | interface Ethernet1/8 47 | 48 | interface Ethernet1/9 49 | 50 | interface Ethernet1/10 51 | 52 | interface Ethernet1/11 53 | 54 | interface Ethernet1/12 55 | 56 | interface Ethernet1/13 57 | 58 | interface Ethernet1/14 59 | 60 | interface Ethernet1/15 61 | 62 | interface Ethernet1/16 63 | 64 | interface Ethernet1/17 65 | 66 | interface Ethernet1/18 67 | 68 | interface Ethernet1/19 69 | 70 | interface Ethernet1/20 71 | 72 | interface Ethernet1/21 73 | 74 | interface Ethernet1/22 75 | 76 | interface Ethernet1/23 77 | 78 | interface Ethernet1/24 79 | 80 | interface Ethernet1/25 81 | 82 | interface Ethernet1/26 83 | 84 | interface Ethernet1/27 85 | 86 | interface Ethernet1/28 87 | 88 | interface Ethernet1/29 89 | 90 | interface Ethernet1/30 91 | 92 | interface Ethernet1/31 93 | 94 | interface Ethernet1/32 95 | 96 | interface Ethernet1/33 97 | 98 | interface Ethernet1/34 99 | 100 | interface Ethernet1/35 101 | 102 | interface Ethernet1/36 103 | 104 | interface Ethernet1/37 105 | 106 | interface Ethernet1/38 107 | 108 | interface Ethernet1/39 109 | 110 | interface Ethernet1/40 111 | 112 | interface Ethernet1/41 113 | 114 | interface Ethernet1/42 115 | 116 | interface Ethernet1/43 117 | 118 | interface Ethernet1/44 119 | 120 | interface Ethernet1/45 121 | 122 | interface Ethernet1/46 123 | 124 | interface Ethernet1/47 125 | 126 | interface Ethernet1/48 127 | 128 | interface Ethernet1/49 129 | 130 | interface Ethernet1/50 131 | 132 | interface Ethernet1/51 133 | 134 | interface Ethernet1/52 135 | 136 | interface Ethernet1/53 137 | 138 | interface Ethernet1/54 139 | 140 | interface Ethernet1/55 141 | 142 | interface Ethernet1/56 143 | 144 | interface Ethernet1/57 145 | 146 | interface Ethernet1/58 147 | 148 | interface Ethernet1/59 149 | 150 | interface Ethernet1/60 151 | 152 | interface Ethernet1/61 153 | 154 | interface Ethernet1/62 155 | 156 | interface Ethernet1/63 157 | 158 | interface Ethernet1/64 159 | 160 | interface Ethernet1/65 161 | 162 | interface Ethernet1/66 163 | 164 | interface Ethernet1/67 165 | 166 | interface Ethernet1/68 167 | 168 | interface Ethernet1/69 169 | 170 | interface Ethernet1/70 171 | 172 | interface Ethernet1/71 173 | 174 | interface Ethernet1/72 175 | 176 | interface Ethernet1/73 177 | 178 | interface Ethernet1/74 179 | 180 | interface Ethernet1/75 181 | 182 | interface Ethernet1/76 183 | 184 | interface Ethernet1/77 185 | 186 | interface Ethernet1/78 187 | 188 | interface Ethernet1/79 189 | 190 | interface Ethernet1/80 191 | 192 | interface Ethernet1/81 193 | 194 | interface Ethernet1/82 195 | 196 | interface Ethernet1/83 197 | 198 | interface Ethernet1/84 199 | 200 | interface Ethernet1/85 201 | 202 | interface Ethernet1/86 203 | 204 | interface Ethernet1/87 205 | 206 | interface Ethernet1/88 207 | 208 | interface Ethernet1/89 209 | 210 | interface Ethernet1/90 211 | 212 | interface Ethernet1/91 213 | 214 | interface Ethernet1/92 215 | 216 | interface Ethernet1/93 217 | 218 | interface Ethernet1/94 219 | 220 | interface Ethernet1/95 221 | 222 | interface Ethernet1/96 223 | 224 | interface Ethernet1/97 225 | 226 | interface Ethernet1/98 227 | 228 | interface Ethernet1/99 229 | 230 | interface Ethernet1/100 231 | 232 | interface Ethernet1/101 233 | 234 | interface Ethernet1/102 235 | 236 | interface Ethernet1/103 237 | 238 | interface Ethernet1/104 239 | 240 | interface Ethernet1/105 241 | 242 | interface Ethernet1/106 243 | 244 | interface Ethernet1/107 245 | 246 | interface Ethernet1/108 247 | 248 | interface Ethernet1/109 249 | 250 | interface Ethernet1/110 251 | 252 | interface Ethernet1/111 253 | 254 | interface Ethernet1/112 255 | 256 | interface Ethernet1/113 257 | 258 | interface Ethernet1/114 259 | 260 | interface Ethernet1/115 261 | 262 | interface Ethernet1/116 263 | 264 | interface Ethernet1/117 265 | 266 | interface Ethernet1/118 267 | 268 | interface Ethernet1/119 269 | 270 | interface Ethernet1/120 271 | 272 | interface Ethernet1/121 273 | 274 | interface Ethernet1/122 275 | 276 | interface Ethernet1/123 277 | 278 | interface Ethernet1/124 279 | 280 | interface Ethernet1/125 281 | 282 | interface Ethernet1/126 283 | 284 | interface Ethernet1/127 285 | 286 | interface Ethernet1/128 287 | 288 | interface mgmt0 289 | vrf member management 290 | ip address 10.0.0.15/24 291 | line console 292 | line vty 293 | -------------------------------------------------------------------------------- /tests/functional/cisco_nxos/expected_output/show_run_no_strip: -------------------------------------------------------------------------------- 1 | !Command: show running-config 2 | !Running configuration last done at: Sun Sep 1 02:24:31 2019 3 | !Time: Sun Sep 1 02:27:46 2019 4 | 5 | version 9.2(4) Bios:version 6 | vdc switch id 1 7 | limit-resource vlan minimum 16 maximum 4094 8 | limit-resource vrf minimum 2 maximum 4096 9 | limit-resource port-channel minimum 0 maximum 511 10 | limit-resource u4route-mem minimum 128 maximum 128 11 | limit-resource u6route-mem minimum 96 maximum 96 12 | limit-resource m4route-mem minimum 58 maximum 58 13 | limit-resource m6route-mem minimum 8 maximum 8 14 | no password strength-check 15 | username admin password 5 $5$DICBLF$Xhpg8rM1hb46a98tsxe249e9m6oa3TH00Y8C40.8JDC role network-admin 16 | username vrnetlab password 5 $5$tGG3FFLY$YXOnmUXhkQE5Vn6KCOzpZBABLFvEzochGeH87YOh2q7 role network-admin 17 | username vrnetlab passphrase lifetime 99999 warntime 14 gracetime 3 18 | ip domain-lookup 19 | copp profile strict 20 | snmp-server user admin network-admin auth md5 0x2d0b9ec17b5d1c3bc718bb33c08528a1 priv 0x2d0b9ec17b5d1c3bc718bb33c08528a1 localizedkey 21 | snmp-server user vrnetlab network-admin auth md5 0x3b9301da0b9f328bc2573fed20773dcb priv 0x3b9301da0b9f328bc2573fed20773dcb localizedkey 22 | rmon event 1 description FATAL(1) owner PMON@FATAL 23 | rmon event 2 description CRITICAL(2) owner PMON@CRITICAL 24 | rmon event 3 description ERROR(3) owner PMON@ERROR 25 | rmon event 4 description WARNING(4) owner PMON@WARNING 26 | rmon event 5 description INFORMATION(5) owner PMON@INFO 27 | 28 | vlan 1 29 | 30 | vrf context management 31 | 32 | interface Ethernet1/1 33 | 34 | interface Ethernet1/2 35 | 36 | interface Ethernet1/3 37 | 38 | interface Ethernet1/4 39 | 40 | interface Ethernet1/5 41 | 42 | interface Ethernet1/6 43 | 44 | interface Ethernet1/7 45 | 46 | interface Ethernet1/8 47 | 48 | interface Ethernet1/9 49 | 50 | interface Ethernet1/10 51 | 52 | interface Ethernet1/11 53 | 54 | interface Ethernet1/12 55 | 56 | interface Ethernet1/13 57 | 58 | interface Ethernet1/14 59 | 60 | interface Ethernet1/15 61 | 62 | interface Ethernet1/16 63 | 64 | interface Ethernet1/17 65 | 66 | interface Ethernet1/18 67 | 68 | interface Ethernet1/19 69 | 70 | interface Ethernet1/20 71 | 72 | interface Ethernet1/21 73 | 74 | interface Ethernet1/22 75 | 76 | interface Ethernet1/23 77 | 78 | interface Ethernet1/24 79 | 80 | interface Ethernet1/25 81 | 82 | interface Ethernet1/26 83 | 84 | interface Ethernet1/27 85 | 86 | interface Ethernet1/28 87 | 88 | interface Ethernet1/29 89 | 90 | interface Ethernet1/30 91 | 92 | interface Ethernet1/31 93 | 94 | interface Ethernet1/32 95 | 96 | interface Ethernet1/33 97 | 98 | interface Ethernet1/34 99 | 100 | interface Ethernet1/35 101 | 102 | interface Ethernet1/36 103 | 104 | interface Ethernet1/37 105 | 106 | interface Ethernet1/38 107 | 108 | interface Ethernet1/39 109 | 110 | interface Ethernet1/40 111 | 112 | interface Ethernet1/41 113 | 114 | interface Ethernet1/42 115 | 116 | interface Ethernet1/43 117 | 118 | interface Ethernet1/44 119 | 120 | interface Ethernet1/45 121 | 122 | interface Ethernet1/46 123 | 124 | interface Ethernet1/47 125 | 126 | interface Ethernet1/48 127 | 128 | interface Ethernet1/49 129 | 130 | interface Ethernet1/50 131 | 132 | interface Ethernet1/51 133 | 134 | interface Ethernet1/52 135 | 136 | interface Ethernet1/53 137 | 138 | interface Ethernet1/54 139 | 140 | interface Ethernet1/55 141 | 142 | interface Ethernet1/56 143 | 144 | interface Ethernet1/57 145 | 146 | interface Ethernet1/58 147 | 148 | interface Ethernet1/59 149 | 150 | interface Ethernet1/60 151 | 152 | interface Ethernet1/61 153 | 154 | interface Ethernet1/62 155 | 156 | interface Ethernet1/63 157 | 158 | interface Ethernet1/64 159 | 160 | interface Ethernet1/65 161 | 162 | interface Ethernet1/66 163 | 164 | interface Ethernet1/67 165 | 166 | interface Ethernet1/68 167 | 168 | interface Ethernet1/69 169 | 170 | interface Ethernet1/70 171 | 172 | interface Ethernet1/71 173 | 174 | interface Ethernet1/72 175 | 176 | interface Ethernet1/73 177 | 178 | interface Ethernet1/74 179 | 180 | interface Ethernet1/75 181 | 182 | interface Ethernet1/76 183 | 184 | interface Ethernet1/77 185 | 186 | interface Ethernet1/78 187 | 188 | interface Ethernet1/79 189 | 190 | interface Ethernet1/80 191 | 192 | interface Ethernet1/81 193 | 194 | interface Ethernet1/82 195 | 196 | interface Ethernet1/83 197 | 198 | interface Ethernet1/84 199 | 200 | interface Ethernet1/85 201 | 202 | interface Ethernet1/86 203 | 204 | interface Ethernet1/87 205 | 206 | interface Ethernet1/88 207 | 208 | interface Ethernet1/89 209 | 210 | interface Ethernet1/90 211 | 212 | interface Ethernet1/91 213 | 214 | interface Ethernet1/92 215 | 216 | interface Ethernet1/93 217 | 218 | interface Ethernet1/94 219 | 220 | interface Ethernet1/95 221 | 222 | interface Ethernet1/96 223 | 224 | interface Ethernet1/97 225 | 226 | interface Ethernet1/98 227 | 228 | interface Ethernet1/99 229 | 230 | interface Ethernet1/100 231 | 232 | interface Ethernet1/101 233 | 234 | interface Ethernet1/102 235 | 236 | interface Ethernet1/103 237 | 238 | interface Ethernet1/104 239 | 240 | interface Ethernet1/105 241 | 242 | interface Ethernet1/106 243 | 244 | interface Ethernet1/107 245 | 246 | interface Ethernet1/108 247 | 248 | interface Ethernet1/109 249 | 250 | interface Ethernet1/110 251 | 252 | interface Ethernet1/111 253 | 254 | interface Ethernet1/112 255 | 256 | interface Ethernet1/113 257 | 258 | interface Ethernet1/114 259 | 260 | interface Ethernet1/115 261 | 262 | interface Ethernet1/116 263 | 264 | interface Ethernet1/117 265 | 266 | interface Ethernet1/118 267 | 268 | interface Ethernet1/119 269 | 270 | interface Ethernet1/120 271 | 272 | interface Ethernet1/121 273 | 274 | interface Ethernet1/122 275 | 276 | interface Ethernet1/123 277 | 278 | interface Ethernet1/124 279 | 280 | interface Ethernet1/125 281 | 282 | interface Ethernet1/126 283 | 284 | interface Ethernet1/127 285 | 286 | interface Ethernet1/128 287 | 288 | interface mgmt0 289 | vrf member management 290 | ip address 10.0.0.15/24 291 | line console 292 | line vty 293 | 294 | 295 | 296 | 297 | switch# 298 | -------------------------------------------------------------------------------- /tests/functional/cisco_nxos/ext_test_funcs.py: -------------------------------------------------------------------------------- 1 | def nxos_disable_paging(cls): 2 | cls.send_inputs("term length 0") 3 | -------------------------------------------------------------------------------- /tests/functional/cisco_nxos/test_nxos.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import re 3 | 4 | import pytest 5 | 6 | from tests.functional.base_functional_tests import BaseFunctionalTest 7 | import ssh2net 8 | 9 | TEST_DEVICE = {"setup_host": "172.18.0.12", "auth_user": "vrnetlab", "auth_password": "VR-netlab9"} 10 | 11 | dummy_conn = ssh2net.NXOSDriver(**TEST_DEVICE) 12 | PRIV_LEVELS = dummy_conn.privs 13 | # remove exec priv level as it is not configured on the vrnetlab host for testing 14 | PRIV_LEVELS.pop("exec") 15 | 16 | 17 | class TestNXOS(BaseFunctionalTest): 18 | def setup_method(self): 19 | self.platform_driver = ssh2net.NXOSDriver 20 | 21 | self.device_type = Path(__file__).resolve().parts[-2] 22 | self.func_test_dir = ( 23 | f"{Path(ssh2net.__file__).parents[1]}/tests/functional/{self.device_type}/" 24 | ) 25 | self.test_device = TEST_DEVICE 26 | self.disable_paging_ext_function = f"tests.functional.{self.device_type}.ext_test_funcs.{self.device_type.split('_')[1]}_disable_paging" 27 | 28 | @staticmethod 29 | def _replace_trailing_chars_running_config(input_data): 30 | execute_trailing_chars_pattern = re.compile(r"^line vty.*$", flags=re.M | re.I | re.S) 31 | input_data = re.sub(execute_trailing_chars_pattern, "line vty", input_data) 32 | return input_data 33 | 34 | @staticmethod 35 | def _replace_timestamps(input_data): 36 | datetime_pattern = re.compile( 37 | r"(mon|tue|wed|thu|fri|sat|sun)\s+(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s+\d+\s+\d+:\d+:\d+\s\d+", 38 | flags=re.M | re.I, 39 | ) 40 | input_data = re.sub(datetime_pattern, "TIME_STAMP_REPLACED", input_data) 41 | return input_data 42 | 43 | @staticmethod 44 | def _replace_crypto_strings(input_data): 45 | crypto_pattern = re.compile(r"^(.*?\s(?:5|md5)\s)[\w$\.\/]+.*$", flags=re.M | re.I) 46 | input_data = re.sub(crypto_pattern, "CRYPTO_REPLACED", input_data) 47 | return input_data 48 | 49 | def disable_paging(self, cls): 50 | cls.send_inputs("term length 0") 51 | 52 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 53 | def test_send_inputs_interact(self, setup_use_paramiko): 54 | """ 55 | expected output for this test is somewhat massaged to make this test pass 56 | in "real" device ssh session there aren't new lines between response and the prompt 57 | there is also a space after the prompt and before the "n" 58 | given that this is just for interactive "solving" these problems is not worth the effort 59 | """ 60 | interact = ["delete bootflash:virtual-instance.conf", "(yes/no/abort) [y]", "n"] 61 | super().test_send_inputs_interact(setup_use_paramiko, interact) 62 | 63 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 64 | @pytest.mark.parametrize("priv_level", [priv for priv in PRIV_LEVELS.values()]) 65 | def test_acquire_all_priv_levels(self, setup_use_paramiko, priv_level): 66 | super().test_acquire_all_priv_levels(setup_use_paramiko, priv_level) 67 | 68 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 69 | def test__determine_current_priv_special_configuration(self, setup_use_paramiko): 70 | with self.platform_driver( 71 | **self.test_device, setup_use_paramiko=setup_use_paramiko 72 | ) as conn: 73 | conn.send_inputs(["configure terminal", "interface Ethernet1/128"]) 74 | current_prompt = conn.get_prompt() 75 | current_priv = conn._determine_current_priv(current_prompt) 76 | assert current_priv.name == "special_configuration" 77 | -------------------------------------------------------------------------------- /tests/functional/comparison_tests/test_comparison_tests.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import subprocess 3 | 4 | import ssh2net 5 | 6 | 7 | NET2_DIR = ssh2net.__file__ 8 | COMPARISON_TEST_DIR = f"{Path(NET2_DIR).parents[1]}/comparison_tests/" 9 | 10 | 11 | def subprocess_runner(cmd, cwd): 12 | with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd) as proc: 13 | std_out, std_err = proc.communicate() 14 | return (std_out.decode(), std_err.decode(), proc.returncode) 15 | 16 | 17 | def test_test_ssh2net(): 18 | cmd = ["python", "test_ssh2net.py"] 19 | std_out, std_err, return_code = subprocess_runner(cmd, COMPARISON_TEST_DIR) 20 | assert return_code == 0 21 | assert std_err == "" 22 | 23 | 24 | def test_test_netmiko(): 25 | cmd = ["python", "test_netmiko.py"] 26 | std_out, std_err, return_code = subprocess_runner(cmd, COMPARISON_TEST_DIR) 27 | assert return_code == 0 28 | assert std_err == "" 29 | -------------------------------------------------------------------------------- /tests/functional/examples/README.md: -------------------------------------------------------------------------------- 1 | put tests for any examples here! 2 | -------------------------------------------------------------------------------- /tests/functional/juniper_junos/expected_output/interactive: -------------------------------------------------------------------------------- 1 | Password: 2 | root@% 3 | -------------------------------------------------------------------------------- /tests/functional/juniper_junos/expected_output/show_run: -------------------------------------------------------------------------------- 1 | ## Last commit: 2019-09-04 00:07:59 UTC by root 2 | version 17.3R2.10; 3 | system { 4 | root-authentication { 5 | encrypted-password "$6$H3raU2MQ$9y6Tm/QPYA9Um2U2UuajFp1RcSIs7q8i9q/9CBmQK1FZQMGqYeMtSlgrYlH2x/FS/703Z5hRyDTUQQDvvYaA9/"; ## SECRET-DATA 6 | } 7 | login { 8 | user vrnetlab { 9 | uid 2000; 10 | class super-user; 11 | authentication { 12 | encrypted-password "$6$ay21MJjL$mFcINECEm2FXQZsTp4gnMmvYiaUo1KplHYuGJLKN0B0qVCW8N0M4ZN5Aci/JCwJbgM57b9Zs/sELszBra7O5C."; ## SECRET-DATA 13 | } 14 | } 15 | } 16 | services { 17 | ssh { 18 | protocol-version v2; 19 | } 20 | netconf { 21 | ssh; 22 | } 23 | web-management { 24 | http { 25 | interface fxp0.0; 26 | } 27 | } 28 | } 29 | syslog { 30 | user * { 31 | any emergency; 32 | } 33 | file messages { 34 | any any; 35 | authorization info; 36 | } 37 | file interactive-commands { 38 | interactive-commands any; 39 | } 40 | } 41 | license { 42 | autoupdate { 43 | url https://ae1.juniper.net/junos/key_retrieval; 44 | } 45 | } 46 | } 47 | security { 48 | screen { 49 | ids-option untrust-screen { 50 | icmp { 51 | ping-death; 52 | } 53 | ip { 54 | source-route-option; 55 | tear-drop; 56 | } 57 | tcp { 58 | syn-flood { 59 | alarm-threshold 1024; 60 | attack-threshold 200; 61 | source-threshold 1024; 62 | destination-threshold 2048; 63 | queue-size 2000; ## Warning: 'queue-size' is deprecated 64 | timeout 20; 65 | } 66 | land; 67 | } 68 | } 69 | } 70 | policies { 71 | from-zone trust to-zone trust { 72 | policy default-permit { 73 | match { 74 | source-address any; 75 | destination-address any; 76 | application any; 77 | } 78 | then { 79 | permit; 80 | } 81 | } 82 | } 83 | from-zone trust to-zone untrust { 84 | policy default-permit { 85 | match { 86 | source-address any; 87 | destination-address any; 88 | application any; 89 | } 90 | then { 91 | permit; 92 | } 93 | } 94 | } 95 | } 96 | zones { 97 | security-zone trust { 98 | tcp-rst; 99 | } 100 | security-zone untrust { 101 | screen untrust-screen; 102 | } 103 | } 104 | } 105 | interfaces { 106 | fxp0 { 107 | unit 0 { 108 | family inet { 109 | address 10.0.0.15/24; 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/functional/juniper_junos/expected_output/show_run_no_strip: -------------------------------------------------------------------------------- 1 | ## Last commit: 2019-09-04 00:07:59 UTC by root 2 | version 17.3R2.10; 3 | system { 4 | root-authentication { 5 | encrypted-password "$6$H3raU2MQ$9y6Tm/QPYA9Um2U2UuajFp1RcSIs7q8i9q/9CBmQK1FZQMGqYeMtSlgrYlH2x/FS/703Z5hRyDTUQQDvvYaA9/"; ## SECRET-DATA 6 | } 7 | login { 8 | user vrnetlab { 9 | uid 2000; 10 | class super-user; 11 | authentication { 12 | encrypted-password "$6$ay21MJjL$mFcINECEm2FXQZsTp4gnMmvYiaUo1KplHYuGJLKN0B0qVCW8N0M4ZN5Aci/JCwJbgM57b9Zs/sELszBra7O5C."; ## SECRET-DATA 13 | } 14 | } 15 | } 16 | services { 17 | ssh { 18 | protocol-version v2; 19 | } 20 | netconf { 21 | ssh; 22 | } 23 | web-management { 24 | http { 25 | interface fxp0.0; 26 | } 27 | } 28 | } 29 | syslog { 30 | user * { 31 | any emergency; 32 | } 33 | file messages { 34 | any any; 35 | authorization info; 36 | } 37 | file interactive-commands { 38 | interactive-commands any; 39 | } 40 | } 41 | license { 42 | autoupdate { 43 | url https://ae1.juniper.net/junos/key_retrieval; 44 | } 45 | } 46 | } 47 | security { 48 | screen { 49 | ids-option untrust-screen { 50 | icmp { 51 | ping-death; 52 | } 53 | ip { 54 | source-route-option; 55 | tear-drop; 56 | } 57 | tcp { 58 | syn-flood { 59 | alarm-threshold 1024; 60 | attack-threshold 200; 61 | source-threshold 1024; 62 | destination-threshold 2048; 63 | queue-size 2000; ## Warning: 'queue-size' is deprecated 64 | timeout 20; 65 | } 66 | land; 67 | } 68 | } 69 | } 70 | policies { 71 | from-zone trust to-zone trust { 72 | policy default-permit { 73 | match { 74 | source-address any; 75 | destination-address any; 76 | application any; 77 | } 78 | then { 79 | permit; 80 | } 81 | } 82 | } 83 | from-zone trust to-zone untrust { 84 | policy default-permit { 85 | match { 86 | source-address any; 87 | destination-address any; 88 | application any; 89 | } 90 | then { 91 | permit; 92 | } 93 | } 94 | } 95 | } 96 | zones { 97 | security-zone trust { 98 | tcp-rst; 99 | } 100 | security-zone untrust { 101 | screen untrust-screen; 102 | } 103 | } 104 | } 105 | interfaces { 106 | fxp0 { 107 | unit 0 { 108 | family inet { 109 | address 10.0.0.15/24; 110 | } 111 | } 112 | } 113 | } 114 | 115 | vrnetlab> 116 | -------------------------------------------------------------------------------- /tests/functional/juniper_junos/ext_test_funcs.py: -------------------------------------------------------------------------------- 1 | def junos_disable_paging(cls): 2 | cls.send_inputs("set cli screen-length 0") 3 | -------------------------------------------------------------------------------- /tests/functional/juniper_junos/test_junos.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import re 3 | 4 | import pytest 5 | 6 | from tests.functional.base_functional_tests import BaseFunctionalTest 7 | import ssh2net 8 | 9 | TEST_DEVICE = { 10 | "setup_host": "172.18.0.15", 11 | "auth_user": "vrnetlab", 12 | "auth_password": "VR-netlab9", 13 | "comms_disable_paging": "set cli screen-length 0", 14 | } 15 | 16 | dummy_conn = ssh2net.JunosDriver(**TEST_DEVICE) 17 | PRIV_LEVELS = dummy_conn.privs 18 | 19 | 20 | class TestJunos(BaseFunctionalTest): 21 | def setup_method(self): 22 | self.platform_driver = ssh2net.JunosDriver 23 | 24 | self.device_type = Path(__file__).resolve().parts[-2] 25 | self.func_test_dir = ( 26 | f"{Path(ssh2net.__file__).parents[1]}/tests/functional/{self.device_type}/" 27 | ) 28 | self.test_device = TEST_DEVICE 29 | self.disable_paging_ext_function = f"tests.functional.{self.device_type}.ext_test_funcs.{self.device_type.split('_')[1]}_disable_paging" 30 | 31 | @staticmethod 32 | def _replace_trailing_chars_running_config(input_data): 33 | execute_trailing_chars_pattern = re.compile(r"^}\s$", flags=re.M | re.I | re.S) 34 | input_data = re.sub(execute_trailing_chars_pattern, "}", input_data) 35 | return input_data 36 | 37 | @staticmethod 38 | def _replace_timestamps(input_data): 39 | datetime_pattern = re.compile( 40 | r"^## Last commit: \d+-\d+-\d+\s\d+:\d+:\d+\s\w+.*$", flags=re.M | re.I 41 | ) 42 | input_data = re.sub(datetime_pattern, "TIME_STAMP_REPLACED", input_data) 43 | return input_data 44 | 45 | @staticmethod 46 | def _replace_crypto_strings(input_data): 47 | crypto_pattern = re.compile( 48 | r'^\s+encrypted-password\s"[\w$\.\/]+";\s.*$', flags=re.M | re.I 49 | ) 50 | input_data = re.sub(crypto_pattern, "CRYPTO_REPLACED", input_data) 51 | return input_data 52 | 53 | def _disable_paging_function(self, setup_use_paramiko): 54 | test_device_copy = self.test_device.copy() 55 | test_device_copy.pop("comms_disable_paging") 56 | with ssh2net.SSH2Net( 57 | **test_device_copy, 58 | setup_use_paramiko=setup_use_paramiko, 59 | comms_disable_paging=self.disable_paging, 60 | ) as conn: 61 | show_run = conn.send_inputs("show configuration")[0] 62 | show_run = self.clean_input_data(show_run) 63 | return show_run 64 | 65 | def _disable_paging_external_function(self, setup_use_paramiko): 66 | test_device_copy = self.test_device.copy() 67 | test_device_copy.pop("comms_disable_paging") 68 | with ssh2net.SSH2Net( 69 | **test_device_copy, 70 | setup_use_paramiko=setup_use_paramiko, 71 | comms_disable_paging=self.disable_paging_ext_function, 72 | ) as conn: 73 | show_run = conn.send_inputs("show configuration")[0] 74 | show_run = self.clean_input_data(show_run) 75 | return show_run 76 | 77 | def disable_paging(self, cls): 78 | cls.send_inputs("set cli screen-length 0") 79 | 80 | def test_show_run_execute(self): 81 | pytest.skip("no support for execute on junos") 82 | 83 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 84 | def test_show_run_inputs(self, setup_use_paramiko): 85 | command = "show configuration" 86 | super().test_show_run_inputs(setup_use_paramiko, command) 87 | 88 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 89 | def test_show_run_inputs_no_strip_prompt(self, setup_use_paramiko): 90 | command = "show configuration" 91 | super().test_show_run_inputs_no_strip_prompt(setup_use_paramiko, command) 92 | 93 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 94 | def test_send_inputs_interact(self, setup_use_paramiko): 95 | interact = ["start shell user root", "Password:", TEST_DEVICE["auth_password"], r"^root@%$"] 96 | super().test_send_inputs_interact(setup_use_paramiko, interact, hidden_response=True) 97 | 98 | @pytest.mark.parametrize("setup_use_paramiko", [False, True], ids=["ssh2", "paramiko"]) 99 | @pytest.mark.parametrize("priv_level", [priv for priv in PRIV_LEVELS.values()]) 100 | def test_acquire_all_priv_levels(self, setup_use_paramiko, priv_level): 101 | super().test_acquire_all_priv_levels(setup_use_paramiko, priv_level) 102 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlmontanari/ssh2net/55e969b6d44ec3f2bd2ebbd8dedd68b99bee4c5b/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/_ssh_config: -------------------------------------------------------------------------------- 1 | Host 1.2.3.4 someswitch1 2 | User carl 3 | IdentityFile ~/.ssh/mysshkey 4 | IdentitiesOnly yes 5 | Hostname someswitch1.bogus.com 6 | Port 1234 7 | 8 | Host * 9 | User carl 10 | 11 | Host someswitch? 12 | User carl 13 | IdentityFile ~/.ssh/mysshkey 14 | IdentitiesOnly yes 15 | Hostname someswitch1.bogus.com 16 | Port 1234 17 | 18 | -------------------------------------------------------------------------------- /tests/unit/drivers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlmontanari/ssh2net/55e969b6d44ec3f2bd2ebbd8dedd68b99bee4c5b/tests/unit/drivers/__init__.py -------------------------------------------------------------------------------- /tests/unit/drivers/base_driver_unit_tests.py: -------------------------------------------------------------------------------- 1 | class BaseDriverUnitTest: 2 | def setup_method(self): 3 | pass 4 | 5 | def get_prompt(self): 6 | return self.standard_prompt 7 | 8 | @staticmethod 9 | def send_inputs(): 10 | return True 11 | 12 | @staticmethod 13 | def send_inputs_interact(): 14 | return True 15 | 16 | def test__determine_current_priv_exec(self): 17 | assert self.driver._determine_current_priv("myrouter>") == self.privs["exec"] 18 | 19 | def test__determine_current_priv_privilege_exec(self): 20 | assert self.driver._determine_current_priv("myrouter#") == self.privs["privilege_exec"] 21 | 22 | def test__determine_current_priv_config(self): 23 | assert ( 24 | self.driver._determine_current_priv("myrouter(config)#") == self.privs["configuration"] 25 | ) 26 | 27 | def test__determine_current_priv_special_config(self): 28 | assert ( 29 | self.driver._determine_current_priv("myrouter(config-if)#") 30 | == self.privs["special_configuration"] 31 | ) 32 | -------------------------------------------------------------------------------- /tests/unit/drivers/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlmontanari/ssh2net/55e969b6d44ec3f2bd2ebbd8dedd68b99bee4c5b/tests/unit/drivers/core/__init__.py -------------------------------------------------------------------------------- /tests/unit/drivers/core/test_core_arista_eos_driver.py: -------------------------------------------------------------------------------- 1 | from ssh2net.core.arista_eos.driver import EOSDriver, PRIVS 2 | from tests.unit.drivers.base_driver_unit_tests import BaseDriverUnitTest 3 | 4 | 5 | class TestEOS(BaseDriverUnitTest): 6 | def setup_method(self): 7 | self.privs = PRIVS 8 | self.driver = EOSDriver() 9 | -------------------------------------------------------------------------------- /tests/unit/drivers/core/test_core_cisco_iosxe_driver.py: -------------------------------------------------------------------------------- 1 | from ssh2net.core.cisco_iosxe.driver import IOSXEDriver, PRIVS 2 | from tests.unit.drivers.base_driver_unit_tests import BaseDriverUnitTest 3 | 4 | 5 | class TestIOSXE(BaseDriverUnitTest): 6 | def setup_method(self): 7 | self.privs = PRIVS 8 | self.driver = IOSXEDriver() 9 | -------------------------------------------------------------------------------- /tests/unit/drivers/core/test_core_cisco_iosxr_driver.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ssh2net.core.cisco_iosxr.driver import IOSXRDriver, PRIVS 4 | from tests.unit.drivers.base_driver_unit_tests import BaseDriverUnitTest 5 | 6 | 7 | class TestIOSXR(BaseDriverUnitTest): 8 | def setup_method(self): 9 | self.privs = PRIVS 10 | self.driver = IOSXRDriver() 11 | 12 | def test__determine_current_priv_exec(self): 13 | pytest.skip("no privilege exec on iosxr") 14 | -------------------------------------------------------------------------------- /tests/unit/drivers/core/test_core_cisco_nxos_driver.py: -------------------------------------------------------------------------------- 1 | from ssh2net.core.cisco_nxos.driver import NXOSDriver, PRIVS 2 | from tests.unit.drivers.base_driver_unit_tests import BaseDriverUnitTest 3 | 4 | 5 | class TestNXOS(BaseDriverUnitTest): 6 | def setup_method(self): 7 | self.privs = PRIVS 8 | self.driver = NXOSDriver() 9 | -------------------------------------------------------------------------------- /tests/unit/drivers/core/test_core_juniper_junos_driver.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ssh2net.core.juniper_junos.driver import JunosDriver, PRIVS 4 | from tests.unit.drivers.base_driver_unit_tests import BaseDriverUnitTest 5 | 6 | 7 | class TestIOSXE(BaseDriverUnitTest): 8 | def setup_method(self): 9 | self.privs = PRIVS 10 | self.driver = JunosDriver() 11 | 12 | def test__determine_current_priv_privilege_exec(self): 13 | pytest.skip("no privilege exec on junos") 14 | 15 | def test__determine_current_priv_config(self): 16 | assert self.driver._determine_current_priv("myrouter#") == self.privs["configuration"] 17 | 18 | def test__determine_current_priv_special_config(self): 19 | pytest.skip("not testing for special config pattern on juniper") 20 | -------------------------------------------------------------------------------- /tests/unit/ext_test_funcs.py: -------------------------------------------------------------------------------- 1 | def some_pre_login_handler_func(cls): 2 | pass 3 | 4 | 5 | def some_disable_paging_func(cls): 6 | pass 7 | -------------------------------------------------------------------------------- /tests/unit/test_channel.py: -------------------------------------------------------------------------------- 1 | from ssh2net import SSH2NetChannel 2 | 3 | 4 | def test__rstrip_all_lines(): 5 | test_input = b""" 6 | some line 7 | another line 8 | one final line 9 | """ 10 | output = SSH2NetChannel._rstrip_all_lines(test_input) 11 | for line in output.splitlines(): 12 | assert line[-1] != " " 13 | 14 | 15 | def test__restructure_output(): 16 | output = "\n\nsomedata\n3560CX#" 17 | output = SSH2NetChannel._restructure_output(output) 18 | assert output.splitlines()[0] != "" 19 | assert output.splitlines()[1] != "" 20 | 21 | 22 | def test__restructure_output_strip_prompt(): 23 | output = "\n\nsomedata\n3560CX#" 24 | output = SSH2NetChannel._restructure_output(output, strip_prompt=True) 25 | assert output.splitlines()[0] != "" 26 | assert output.splitlines()[-1] != "3560CX#" 27 | 28 | 29 | def test__strip_ansi(): 30 | output = b"[admin@CoolDevice.Sea1: \x1b[1m/\x1b[0;0m]$" 31 | output = SSH2NetChannel._strip_ansi(output) 32 | assert output == b"[admin@CoolDevice.Sea1: /]$" 33 | -------------------------------------------------------------------------------- /tests/unit/test_core_driver.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | from ssh2net.exceptions import UnknownPrivLevel 6 | from ssh2net.core.driver import BaseNetworkDriver 7 | from ssh2net.core.cisco_iosxe.driver import PRIVS 8 | 9 | 10 | IOS_ARP = """Protocol Address Age (min) Hardware Addr Type Interface 11 | Internet 172.31.254.1 - 0000.0c07.acfe ARPA Vlan254 12 | Internet 172.31.254.2 - c800.84b2.e9c2 ARPA Vlan254 13 | """ 14 | 15 | 16 | def test__determine_current_priv(): 17 | base_driver = BaseNetworkDriver() 18 | base_driver.privs = PRIVS 19 | current_priv = base_driver._determine_current_priv("execprompt>") 20 | assert current_priv.name == "exec" 21 | 22 | 23 | def test__determine_current_priv_unknown(): 24 | base_driver = BaseNetworkDriver() 25 | base_driver.privs = PRIVS 26 | with pytest.raises(UnknownPrivLevel): 27 | base_driver._determine_current_priv("!!!!thisissoooowrongggg!!!!!!?!") 28 | 29 | 30 | @pytest.mark.skipif(sys.platform.startswith("win"), reason="not supporting textfsm on windows") 31 | def test_textfsm_parse_output(): 32 | base_driver = BaseNetworkDriver() 33 | base_driver.textfsm_platform = "cisco_ios" 34 | result = base_driver.textfsm_parse_output("show ip arp", IOS_ARP) 35 | assert isinstance(result, list) 36 | assert result[0] == ["Internet", "172.31.254.1", "-", "0000.0c07.acfe", "ARPA", "Vlan254"] 37 | -------------------------------------------------------------------------------- /tests/unit/test_decorators.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | 4 | import pytest 5 | 6 | from ssh2net import SSH2Net 7 | 8 | if not sys.platform.startswith("win"): 9 | from ssh2net.decorators import operation_timeout 10 | else: 11 | from ssh2net.decorators import operation_timeout_win as operation_timeout 12 | 13 | 14 | class MockSSH2Net(SSH2Net): 15 | def __init__(self): 16 | super().__init__() 17 | self.comms_operation_timeout = 0.1 18 | 19 | @operation_timeout("comms_operation_timeout") 20 | def operation_timeout_func(self): 21 | time.sleep(1000) 22 | 23 | @operation_timeout("comms_operation_timeout") 24 | def operation_success_func(self): 25 | time.sleep(0.00001) 26 | 27 | 28 | def test_operation_timeout_timeout(): 29 | timeout_test = MockSSH2Net() 30 | with pytest.raises(TimeoutError): 31 | timeout_test.operation_timeout_func() 32 | 33 | 34 | def test_operation_timeout_success(): 35 | timeout_test = MockSSH2Net() 36 | timeout_test.operation_success_func() 37 | 38 | 39 | def test_operation_timeout_no_timeout_value(): 40 | timeout_test = MockSSH2Net() 41 | timeout_test.comms_prompt_timeout = None 42 | timeout_test.operation_success_func() 43 | -------------------------------------------------------------------------------- /tests/unit/test_helper.py: -------------------------------------------------------------------------------- 1 | from io import TextIOWrapper 2 | import pkg_resources 3 | import sys 4 | 5 | import pytest 6 | 7 | from ssh2net.helper import _textfsm_get_template, textfsm_parse 8 | 9 | 10 | IOS_ARP = """Protocol Address Age (min) Hardware Addr Type Interface 11 | Internet 172.31.254.1 - 0000.0c07.acfe ARPA Vlan254 12 | Internet 172.31.254.2 - c800.84b2.e9c2 ARPA Vlan254 13 | """ 14 | 15 | 16 | @pytest.mark.skipif(sys.platform.startswith("win"), reason="not supporting textfsm on windows") 17 | def test__textfsm_get_template_valid_template(): 18 | template = _textfsm_get_template("cisco_nxos", "show ip arp") 19 | template_dir = pkg_resources.resource_filename("ntc_templates", "templates") 20 | assert isinstance(template, TextIOWrapper) 21 | assert template.name == f"{template_dir}/cisco_nxos_show_ip_arp.template" 22 | 23 | 24 | def test__textfsm_get_template_invalid_template(): 25 | template = _textfsm_get_template("cisco_nxos", "show racecar") 26 | assert not template 27 | 28 | 29 | @pytest.mark.skipif(sys.platform.startswith("win"), reason="not supporting textfsm on windows") 30 | def test_text_textfsm_parse_success(): 31 | template = _textfsm_get_template("cisco_ios", "show ip arp") 32 | result = textfsm_parse(template, IOS_ARP) 33 | assert isinstance(result, list) 34 | assert result[0] == ["Internet", "172.31.254.1", "-", "0000.0c07.acfe", "ARPA", "Vlan254"] 35 | 36 | 37 | @pytest.mark.skipif(sys.platform.startswith("win"), reason="not supporting textfsm on windows") 38 | def test_text_textfsm_parse_success_string_path(): 39 | template = _textfsm_get_template("cisco_ios", "show ip arp") 40 | result = textfsm_parse(template.name, IOS_ARP) 41 | assert isinstance(result, list) 42 | assert result[0] == ["Internet", "172.31.254.1", "-", "0000.0c07.acfe", "ARPA", "Vlan254"] 43 | 44 | 45 | @pytest.mark.skipif(sys.platform.startswith("win"), reason="not supporting textfsm on windows") 46 | def test_text_textfsm_parse_failure(): 47 | template = _textfsm_get_template("cisco_ios", "show ip arp") 48 | result = textfsm_parse(template, "not really arp data") 49 | assert isinstance(result, str) 50 | assert result == "not really arp data" 51 | -------------------------------------------------------------------------------- /tests/unit/test_netmiko_compatibility.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ssh2net.netmiko_compatibility import connect_handler, transform_netmiko_kwargs 4 | 5 | 6 | def test_connect_handler_invalid_device_type(): 7 | device = {"device_type": "tacocat"} 8 | with pytest.raises(TypeError): 9 | connect_handler(**device) 10 | 11 | 12 | def test_connect_handler_ip_no_hostname(): 13 | netmiko_args = { 14 | "ip": "1.2.3.4", 15 | "username": "person", 16 | "password": "password", 17 | "port": 123, 18 | "global_delay_factor": 5, 19 | "device_type": "cisco_xe", 20 | } 21 | conn = connect_handler(auto_open=False, **netmiko_args) 22 | assert conn.textfsm_platform == "cisco_ios" 23 | 24 | 25 | def test_connect_handler_valid_connection(): 26 | netmiko_args = { 27 | "host": "1.2.3.4", 28 | "username": "person", 29 | "password": "password", 30 | "port": 123, 31 | "global_delay_factor": 5, 32 | "device_type": "cisco_xe", 33 | } 34 | conn = connect_handler(auto_open=False, **netmiko_args) 35 | assert conn.textfsm_platform == "cisco_ios" 36 | 37 | 38 | def test_transform_netmiko_args(): 39 | netmiko_args = { 40 | "host": "1.2.3.4", 41 | "username": "person", 42 | "password": "password", 43 | "port": 123, 44 | "global_delay_factor": 5, 45 | } 46 | transformed_args = transform_netmiko_kwargs(netmiko_args) 47 | assert transformed_args["setup_host"] == "1.2.3.4" 48 | assert transformed_args["session_timeout"] == 25000 49 | 50 | 51 | def test_transform_netmiko_args_setup_timeout(): 52 | netmiko_args = {"host": "1.2.3.4", "username": "person", "password": "password", "port": 123} 53 | transformed_args = transform_netmiko_kwargs(netmiko_args) 54 | assert transformed_args["setup_host"] == "1.2.3.4" 55 | assert transformed_args["session_timeout"] == 5000 56 | -------------------------------------------------------------------------------- /tests/unit/test_session.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlmontanari/ssh2net/55e969b6d44ec3f2bd2ebbd8dedd68b99bee4c5b/tests/unit/test_session.py -------------------------------------------------------------------------------- /tests/unit/test_ssh_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import sys 4 | 5 | import pytest 6 | 7 | import ssh2net 8 | from ssh2net import SSH2NetSSHConfig 9 | from ssh2net.ssh_config import Host 10 | 11 | 12 | NET2_DIR = ssh2net.__file__ 13 | UNIT_TEST_DIR = f"{Path(NET2_DIR).parents[1]}/tests/unit/" 14 | 15 | 16 | def test_init_ssh_config_file_explicit(): 17 | ssh_conf = SSH2NetSSHConfig(f"{UNIT_TEST_DIR}_ssh_config") 18 | with open(f"{UNIT_TEST_DIR}_ssh_config", "r") as f: 19 | ssh_config_file = f.read() 20 | assert ssh_conf.ssh_config_file == ssh_config_file 21 | 22 | 23 | @pytest.mark.skipif(sys.platform.startswith("win"), reason="not supporting ssh config on windows") 24 | def test_init_ssh_config_file_user(fs): 25 | fs.add_real_file("/etc/ssh/ssh_config", target_path=f"{os.path.expanduser('~')}/.ssh/config") 26 | ssh_conf = SSH2NetSSHConfig() 27 | with open(f"{os.path.expanduser('~')}/.ssh/config", "r") as f: 28 | ssh_config_file = f.read() 29 | assert ssh_conf.ssh_config_file == ssh_config_file 30 | 31 | 32 | @pytest.mark.skipif(sys.platform.startswith("win"), reason="not supporting ssh config on windows") 33 | def test_init_ssh_config_file_system(fs): 34 | fs.add_real_file("/etc/ssh/ssh_config") 35 | ssh_conf = SSH2NetSSHConfig() 36 | with open("/etc/ssh/ssh_config", "r") as f: 37 | ssh_config_file = f.read() 38 | assert ssh_conf.ssh_config_file == ssh_config_file 39 | 40 | 41 | def test_init_ssh_config_file_no_config_file(fs): 42 | ssh_conf = SSH2NetSSHConfig() 43 | assert ssh_conf.hosts is None 44 | 45 | 46 | def test_init_ssh_config_file_no_hosts(): 47 | ssh_conf = SSH2NetSSHConfig(f"{UNIT_TEST_DIR}__init__.py") 48 | assert ssh_conf.hosts is None 49 | 50 | 51 | def test_str(): 52 | ssh_conf = SSH2NetSSHConfig(f"{UNIT_TEST_DIR}_ssh_config") 53 | assert str(ssh_conf) == "SSH2NetSSHConfig Object" 54 | 55 | 56 | def test_repr(): 57 | ssh_conf = SSH2NetSSHConfig(f"{UNIT_TEST_DIR}_ssh_config") 58 | assert repr(ssh_conf) == ( 59 | "SSH2NetSSHConfig {'hosts': {'1.2.3.4 someswitch1': HostEntry {'hosts': '1.2.3.4 " 60 | "someswitch1', 'hostname': 'someswitch1.bogus.com', 'port': '1234', 'user': 'carl', " 61 | "'address_family': None, 'bind_address': None, 'connect_timeout': None, " 62 | "'identities_only': 'yes', 'identity_file': '~/.ssh/mysshkey', 'keyboard_interactive': " 63 | "None, 'password_authentication': None, 'preferred_authentication': None}, '*': HostEntry " 64 | "{'hosts': '*', 'hostname': None, 'port': None, 'user': 'carl', 'address_family': None, " 65 | "'bind_address': None, 'connect_timeout': None, 'identities_only': None, 'identity_file': " 66 | "None, 'keyboard_interactive': None, 'password_authentication': None, " 67 | "'preferred_authentication': None}, 'someswitch?': HostEntry {'hosts': 'someswitch?', " 68 | "'hostname': 'someswitch1.bogus.com', 'port': '1234', 'user': 'carl', 'address_family': " 69 | "None, 'bind_address': None, 'connect_timeout': None, 'identities_only': 'yes', " 70 | "'identity_file': '~/.ssh/mysshkey', 'keyboard_interactive': None, " 71 | "'password_authentication': None, 'preferred_authentication': None}}}" 72 | ) 73 | 74 | 75 | def test_bool_true(): 76 | ssh_conf = SSH2NetSSHConfig(f"{UNIT_TEST_DIR}_ssh_config") 77 | assert bool(ssh_conf) is True 78 | 79 | 80 | def test_bool_false(): 81 | ssh_conf = SSH2NetSSHConfig(f"{UNIT_TEST_DIR}__init__.py") 82 | assert bool(ssh_conf) is False 83 | 84 | 85 | def test_host__str(): 86 | host = Host() 87 | assert str(host) == "Host: None" 88 | 89 | 90 | def test_host__repr(): 91 | ssh_conf = SSH2NetSSHConfig(f"{UNIT_TEST_DIR}_ssh_config") 92 | assert repr(ssh_conf.hosts["1.2.3.4 someswitch1"]) == ( 93 | "HostEntry {'hosts': '1.2.3.4 someswitch1', 'hostname': 'someswitch1.bogus.com', 'port': " 94 | "'1234', 'user': 'carl', 'address_family': None, 'bind_address': None, 'connect_timeout': " 95 | "None, 'identities_only': 'yes', 'identity_file': '~/.ssh/mysshkey', " 96 | "'keyboard_interactive': None, 'password_authentication': None, 'preferred_authentication': " 97 | "None}" 98 | ) 99 | 100 | 101 | def test_host_lookup_exact_host(): 102 | ssh_conf = SSH2NetSSHConfig(f"{UNIT_TEST_DIR}_ssh_config") 103 | host = ssh_conf.lookup("1.2.3.4 someswitch1") 104 | assert repr(host) == ( 105 | "HostEntry {'hosts': '1.2.3.4 someswitch1', 'hostname': 'someswitch1.bogus.com', 'port': " 106 | "'1234', 'user': 'carl', 'address_family': None, 'bind_address': None, 'connect_timeout': " 107 | "None, 'identities_only': 'yes', 'identity_file': '~/.ssh/mysshkey', " 108 | "'keyboard_interactive': None, 'password_authentication': None, 'preferred_authentication':" 109 | " None}" 110 | ) 111 | 112 | 113 | def test_host_lookup_exact_host_in_list(): 114 | ssh_conf = SSH2NetSSHConfig(f"{UNIT_TEST_DIR}_ssh_config") 115 | host = ssh_conf.lookup("someswitch1") 116 | assert repr(host) == ( 117 | "HostEntry {'hosts': '1.2.3.4 someswitch1', 'hostname': 'someswitch1.bogus.com', 'port': " 118 | "'1234', 'user': 'carl', 'address_family': None, 'bind_address': None, 'connect_timeout': " 119 | "None, 'identities_only': 'yes', 'identity_file': '~/.ssh/mysshkey', " 120 | "'keyboard_interactive': None, 'password_authentication': None, 'preferred_authentication':" 121 | " None}" 122 | ) 123 | 124 | 125 | def test_host_lookup_host_fuzzy(): 126 | ssh_conf = SSH2NetSSHConfig(f"{UNIT_TEST_DIR}_ssh_config") 127 | host = ssh_conf.lookup("someswitch2") 128 | assert repr(host) == ( 129 | "HostEntry {'hosts': 'someswitch?', 'hostname': 'someswitch1.bogus.com', 'port': " 130 | "'1234', 'user': 'carl', 'address_family': None, 'bind_address': None, 'connect_timeout': " 131 | "None, 'identities_only': 'yes', 'identity_file': '~/.ssh/mysshkey', " 132 | "'keyboard_interactive': None, 'password_authentication': None, 'preferred_authentication': " 133 | "None}" 134 | ) 135 | 136 | 137 | def test_host_lookup_host_fuzzy_multi_match(): 138 | ssh_conf = SSH2NetSSHConfig(f"{UNIT_TEST_DIR}_ssh_config") 139 | host = ssh_conf.lookup("someswitch9999") 140 | assert repr(host) == ( 141 | "HostEntry {'hosts': 'someswitch?', 'hostname': 'someswitch1.bogus.com', 'port': " 142 | "'1234', 'user': 'carl', 'address_family': None, 'bind_address': None, 'connect_timeout': " 143 | "None, 'identities_only': 'yes', 'identity_file': '~/.ssh/mysshkey', " 144 | "'keyboard_interactive': None, 'password_authentication': None, 'preferred_authentication': " 145 | "None}" 146 | ) 147 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py36,py37,py38 4 | 5 | 6 | [testenv] 7 | deps = 8 | -rrequirements-dev.txt 9 | commands = 10 | python -m pytest tests/unit/. 11 | 12 | 13 | [testenv:py38] 14 | deps = 15 | -rrequirements-dev.txt 16 | commands = 17 | python -m pytest \ 18 | --cov=ssh2net \ 19 | --cov-report html \ 20 | --cov-report term \ 21 | tests/unit/. 22 | python -m black . 23 | python -m pylama . 24 | python -m pydocstyle . 25 | darglint ssh2net/. 26 | --------------------------------------------------------------------------------