├── .github └── workflows │ └── pythonapp.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── iosxenapalmapi.click.py ├── iosxenapalmapi.py ├── new_loopbacks.cfg ├── replace.conf ├── requirements.txt ├── rollback_loopbacks.cfg └── validate.yml /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | name: Python application 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up Python 3.7 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.7 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install -r requirements.txt 20 | - name: Lint with flake8 21 | run: | 22 | pip install flake8 23 | # stop the build if there are Python syntax errors or undefined names 24 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 25 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 26 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 27 | 28 | 29 | - name: Temporary Hacky Webhook Success 30 | if: success() 31 | env: # Set the github secret as an input 32 | teams_webhook_url: ${{ secrets.TEAMS_WEBHOOK_URL }} 33 | run: | 34 | curl -X POST -H "Content-Type: application/json" -d "{\"text\" : \"Github Action CI run succeeded. Workflow: ${GITHUB_WORKFLOW} Action ID: ${GITHUB_ACTION} Started By: ${GITHUB_ACTOR} REPOSITORY: https://github.com/${GITHUB_REPOSITORY} SHA: ${GITHUB_SHA}\"}" ${teams_webhook_url} 35 | - name: Temporary Hacky Webhook Failure 36 | if: failure() 37 | env: # Set the github secret as an input 38 | teams_webhook_url: ${{ secrets.TEAMS_WEBHOOK_URL }} 39 | run: | 40 | curl -X POST -H "Content-Type: application/json" -d "{\"text\" : \"Github Action CI run FAILED. Workflow: ${GITHUB_WORKFLOW} Action ID: ${GITHUB_ACTION} Started By: ${GITHUB_ACTOR} REPOSITORY: https://github.com/${GITHUB_REPOSITORY} SHA: ${GITHUB_SHA}\"}" ${teams_webhook_url} 41 | # - name: Test with pytest 42 | # run: | 43 | # pip install pytest 44 | # pytest 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | The following is a set of guidelines for contributing, I want to make contributing to this project as easy and transparent as possible. Use your best judgment, and feel free to propose changes to this document in a pull request. 4 | 5 | 6 | ## Pull Requests 7 | 8 | 1. Fork the repo and create your branch from `master`. 9 | 2. If you've added/changes to the code, please test this thoroughly. 10 | 11 | 12 | Always write a clear log message for your commits. One-line messages are fine for small changes, but bigger changes should look like this please: 13 | 14 | ``` 15 | $ git commit -m "A brief summary of the commit 16 | > 17 | > A paragraph describing what changed." 18 | ``` 19 | 20 | 21 | Thank you. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Cisco Systems, Inc. and/or its affiliates 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Network Automation with Napalm and Click Python libraries 2 | 3 | Network automation and devices management has many paths and options... With so many APIs being available your network can quickly be as complex to manage with automation as it was with manual configurations. Evaluating the offerings of API vendors often goes past the technology itself. But, [NAPALM](https://napalm.readthedocs.io/en/latest/index.html) (Network Automation and Programmability Abstraction Layer with Multivendor support) is helping to change that, a unified API makes it possible to share code between network devices and vendors. 4 | 5 | NAPALM is a Python library that implements a set of functions to interact with different network device Operating Systems using a unified API. 6 | 7 | [Click](https://click.palletsprojects.com/en/7.x/) is a Python package for creating beautiful command line interfaces in a composable way with as little code as necessary. 8 | 9 | ## DevNet Sandbox 10 | All code has been written/tested on the [Cisco DevNet IOS XR always on Sandbox](https://t.co/V6rXj3plwF). 11 | Please see the sandbox pages for credentials and reservations. 12 | 13 | ## Code 14 | 15 | All of the code you need is located in this repo. Clone the repo and access it with the following commands: 16 | 17 | ``` 18 | git clone https://github.com/bigevilbeard/napalm_click 19 | cd napalm_click 20 | ``` 21 | 22 | ## Python Environment Setup 23 | It is recommended that this code be used with Python 3.6. It is highly recommended to leverage Python Virtual Environments (venv). 24 | 25 | Follow these steps to create and activate a venv. 26 | ``` 27 | # OS X or Linux 28 | virtualenv venv --python=python3.6 29 | source venv/bin/activate 30 | ``` 31 | ## Install the code requirements 32 | ``` 33 | pip install -r requirements.txt 34 | ``` 35 | 36 | 37 | ## Running the code examples 38 | 39 | NAPALM supports several methods to connect to the devices, to manipulate configurations or to retrieve data. Configurations can be replaced entirely or merged into the existing device config. You can load configuration either from a string or from a file. If for some reason you committed the changes and you want to rollback, this can also be done (please check NAPALM documentation for support of merge, replace and rollback as some platforms differ) 40 | 41 | 42 | This code uses Object-Oriented Programming (OOP). This is a programming paradigm where different components of a computer program are modeled after real-world objects. An object is anything that has some characteristics and can perform a function. All args used in the running of the code are handled using [Click](https://click.palletsprojects.com/en/7.x/). 43 | 44 | ``` 45 | cli.add_command(facts) 46 | cli.add_command(interfaces) 47 | cli.add_command(interfaces_ip) 48 | cli.add_command(merge) 49 | cli.add_command(replace) 50 | cli.add_command(rollback) 51 | 52 | ``` 53 | **Note:** Before using the code, update the IP address/hostname and port information. 54 | 55 | ``` 56 | (venv) STUACLAR-M-R6EU:napalm_click stuaclar$ python iosxenapalmapi.click.py --help 57 | Usage: iosxenapalmapi.click.py [OPTIONS] COMMAND [ARGS]... 58 | 59 | Options: 60 | --help Show this message and exit. 61 | 62 | Commands: 63 | facts 64 | interfaces 65 | interfaces_ip 66 | merge 67 | replace 68 | rollback 69 | validation 70 | ``` 71 | 72 | With this code, we show the router version and interface information (shown in `json` format). 73 | 74 | 75 | 76 | ## Example Use Commands 77 | 78 | ``` 79 | (venv) STUACLAR-M-R6EU:napalm_click stuaclar$ python iosxenapalmapi.click.py interfaces 80 | Retrieving Information 81 | { 82 | "Loopback100": { 83 | "description": "", 84 | "is_enabled": true, 85 | "is_up": true, 86 | "last_flapped": -1.0, 87 | "mac_address": "", 88 | "speed": 0 89 | }, 90 | "Loopback200": { 91 | "description": "", 92 | "is_enabled": true, 93 | "is_up": true, 94 | "last_flapped": -1.0, 95 | "mac_address": "", 96 | "speed": 0 97 | }, 98 | "MgmtEth0/RP0/CPU0/0": { 99 | "description": "", 100 | "is_enabled": true, 101 | "is_up": true, 102 | "last_flapped": -1.0, 103 | "mac_address": "08:00:27:82:91:AB", 104 | "speed": 1000 105 | }, 106 | "Null0": { 107 | "description": "", 108 | "is_enabled": true, 109 | "is_up": true, 110 | "last_flapped": -1.0, 111 | "mac_address": "", 112 | "speed": 0 113 | } 114 | } 115 | ``` 116 | ``` 117 | (venv) STUACLAR-M-R6EU:napalm_click stuaclar$ python iosxenapalmapi.click.py merge 118 | Merge Loopback Interfaces 119 | 120 | Diff: 121 | --- 122 | +++ 123 | @@ -10,6 +10,12 @@ 124 | +! 125 | +interface Loopback100 126 | + ipv4 address 1.1.1.100 255.255.255.255 127 | +! 128 | +interface Loopback200 129 | + ipv4 address 1.1.1.200 255.255.255.255 130 | ! 131 | interface MgmtEth0/RP0/CPU0/0 132 | ipv4 address dhcp 133 | 134 | Would you like to commit these changes? [yN]: y 135 | Committing ... 136 | ``` 137 | 138 | ## About me 139 | 140 | Network Automation Developer Advocate for Cisco DevNet. 141 | I'm like Hugh Hefner... minus the mansion, the exotic cars, the girls, the magazine and the money. So basically, I have a robe. 142 | 143 | Find me here: [LinkedIn](https://www.linkedin.com/in/stuarteclark/) / [Twitter](https://twitter.com/bigevilbeard) 144 | -------------------------------------------------------------------------------- /iosxenapalmapi.click.py: -------------------------------------------------------------------------------- 1 | import napalm 2 | import click 3 | import json 4 | 5 | class iosxenapalmapi(object): 6 | def __init__(self, hostname=None, username=None, password=None, optional_args=None): 7 | driver = napalm.get_network_driver('ios-xr') 8 | self.connection = driver(hostname=hostname, username=username, password=password, optional_args={'port':8181}) 9 | 10 | 11 | def connect(self): 12 | self.connection.open() 13 | 14 | def disconnect(self): 15 | self.connection.close() 16 | 17 | def get_facts(self): 18 | self.connect() 19 | facts = self.connection.get_facts() 20 | self.disconnect() 21 | return facts 22 | 23 | def get_validation(self): 24 | self.connect() 25 | facts = self.connection.compliance_report("validate.yml") 26 | self.disconnect() 27 | return facts 28 | 29 | def get_interfaces(self): 30 | self.connect() 31 | facts = self.connection.get_interfaces() 32 | self.disconnect() 33 | return facts 34 | 35 | def get_interfaces_ip(self): 36 | self.connect() 37 | facts = self.connection.get_interfaces_ip() 38 | self.disconnect() 39 | return facts 40 | 41 | def merge_loopbacks(self): 42 | self.connect() 43 | facts = self.connection.load_merge_candidate(filename='new_loopbacks.cfg') 44 | print('\nDiff:') 45 | diff = self.connection.compare_config() 46 | print(diff) 47 | if len(diff) < 1: 48 | print('\nNo Changes Required Closing...') 49 | self.connection.discard_config() 50 | self.disconnect() 51 | exit() 52 | 53 | try: 54 | choice = input("\nWould you like to commit these changes? [yN]: ") 55 | except NameError: 56 | choice = input("\nWould you like to commit these changes? [yN]: ") 57 | if choice == 'y': 58 | print('Committing ...') 59 | self.connection.commit_config() 60 | 61 | else: 62 | print('Discarding ...') 63 | self.connection.discard_config() 64 | self.disconnect() 65 | 66 | def replace_loopbacks(self): 67 | self.connect() 68 | facts = self.connection.load_replace_candidate(filename='replace.conf') 69 | print('\nDiff:') 70 | diff = self.connection.compare_config() 71 | print(diff) 72 | if len(diff) < 1: 73 | print('\nNo Changes Required Closing...') 74 | self.connection.discard_config() 75 | self.disconnect() 76 | exit() 77 | 78 | try: 79 | choice = input("\nWould you like to commit these changes? [yN]: ") 80 | except NameError: 81 | choice = input("\nWould you like to commit these changes? [yN]: ") 82 | if choice == 'y': 83 | print('Committing ...') 84 | self.connection.commit_config() 85 | self.disconnect() 86 | 87 | else: 88 | print('Discarding ...') 89 | self.connection.discard_config() 90 | self.disconnect() 91 | 92 | 93 | def rollback_loopbacks(self): 94 | self.connect() 95 | facts = self.connection.load_merge_candidate(filename='rollback_loopbacks.cfg') 96 | print('\nDiff:') 97 | diff = self.connection.compare_config() 98 | print(diff) 99 | if len(diff) < 1: 100 | print('\nNo Changes Required Closing...') 101 | self.connection.discard_config() 102 | self.disconnect() 103 | exit() 104 | 105 | try: 106 | choice = input("\nWould you like to commit these changes? [yN]: ") 107 | except NameError: 108 | choice = input("\nWould you like to commit these changes? [yN]: ") 109 | if choice == 'y': 110 | print('Committing ...') 111 | self.connection.commit_config() 112 | 113 | else: 114 | print('Discarding ...') 115 | self.connection.discard_config() 116 | self.disconnect() 117 | exit() 118 | 119 | choice = input("\nWould you like to rollback these changes? [yN]: ") 120 | if choice == 'y': 121 | print('Reverting ...') 122 | self.connection.rollback() 123 | self.disconnect() 124 | 125 | else: 126 | print('Discarding ...') 127 | self.connection.discard_config() 128 | self.disconnect() 129 | 130 | # hostname, username, password 131 | # device = iosxenapalmapi("127.0.0.1", "vagrant", "vagrant") 132 | device = iosxenapalmapi("sbx-iosxr-mgmt.cisco.com", "admin", "C1sco12345") 133 | 134 | @click.group() 135 | def cli(): 136 | pass 137 | 138 | @click.command() 139 | def facts(): 140 | click.secho("Retrieving Information") 141 | fact = json.dumps(device.get_facts(), sort_keys=True, indent=4) 142 | click.echo(fact) 143 | 144 | @click.command() 145 | def validation(): 146 | click.secho("Retrieving Information") 147 | fact = json.dumps(device.get_validation(), sort_keys=True, indent=4) 148 | click.echo(fact) 149 | 150 | @click.command() 151 | def interfaces(): 152 | click.secho("Retrieving Information") 153 | interface = json.dumps(device.get_interfaces(), sort_keys=True, indent=4) 154 | click.echo(interface) 155 | 156 | @click.command() 157 | def interfaces_ip(): 158 | click.secho("Retrieving Information") 159 | interface_ip = json.dumps(device.get_interfaces_ip(), sort_keys=True, indent=4) 160 | click.echo(interface_ip) 161 | 162 | @click.command() 163 | def merge(): 164 | click.secho("Merge Loopback Interfaces") 165 | merge_loopbacks = json.dumps(device.merge_loopbacks(), sort_keys=True, indent=4) 166 | click.echo(merge) 167 | 168 | @click.command() 169 | def replace(): 170 | click.secho("Replace Loopback Interfaces") 171 | replace_loopbacks = json.dumps(device.replace_loopbacks(), sort_keys=True, indent=4) 172 | click.echo(replace) 173 | 174 | @click.command() 175 | def rollback(): 176 | click.secho("Rollback Loopback Interfaces") 177 | replace_loopbacks = json.dumps(device.rollback_loopbacks(), sort_keys=True, indent=4) 178 | click.echo(rollback) 179 | 180 | cli.add_command(facts) 181 | cli.add_command(validation) 182 | cli.add_command(interfaces) 183 | cli.add_command(interfaces_ip) 184 | cli.add_command(merge) 185 | cli.add_command(replace) 186 | cli.add_command(rollback) 187 | 188 | if __name__ == "__main__": 189 | cli() 190 | -------------------------------------------------------------------------------- /iosxenapalmapi.py: -------------------------------------------------------------------------------- 1 | import napalm 2 | import json 3 | 4 | class iosxenapalmapi(object): 5 | def __init__(self, hostname=None, username=None, password=None, optional_args=None): 6 | driver = napalm.get_network_driver('ios') 7 | # driver = napalm.get_network_driver('nxos_ssh') 8 | # driver = napalm.get_network_driver('ios-xr') 9 | self.connection = driver(hostname=hostname, username=username, password=password, optional_args={'port':8181}) 10 | 11 | 12 | def connect(self): 13 | self.connection.open() 14 | 15 | def disconnect(self): 16 | self.connection.close() 17 | 18 | 19 | 20 | def get_facts(self): 21 | """Retrieve and return network devices informatiom. 22 | 23 | ./iosxenapalmapi.py get_facts 24 | """ 25 | self.connect() 26 | facts = self.connection.get_facts() 27 | self.disconnect() 28 | return facts 29 | 30 | # def get_interfaces(self): 31 | # """Retrieve and return network devices informatiom. 32 | # ./iosxenapalmapi.py get_interfaces 33 | # """ 34 | # self.connect() 35 | # facts = self.connection.get_interfaces() 36 | # self.disconnect() 37 | # return facts 38 | 39 | # devices 40 | # hostname, username, password 41 | # This example uses the always on devnet sandbox's ios xe, nx-os 42 | # device = iosxenapalmapi("sbx-nxos-mgmt.cisco.com", "admin", "Admin_1234!") 43 | device = iosxenapalmapi("ios-xe-mgmt-latest.cisco.com", "developer", "C1sco12345") 44 | # device = iosxenapalmapi("sbx-iosxr-mgmt.cisco.com", "admin", "C1sco12345") 45 | # device = iosxenapalmapi("ios-xe-mgmt-latest.cisco.com","developer","C1sco12345") 46 | 47 | 48 | # device = iosxenapalmapi("ios-xe-mgmt.cisco.com", "root", "D_Vay!_10&") 49 | # print(device.get_facts()) 50 | print(json.dumps(device.get_facts(), sort_keys=True, indent=4)) 51 | # print(json.dumps(device.get_interfaces(), sort_keys=True, indent=4) 52 | -------------------------------------------------------------------------------- /new_loopbacks.cfg: -------------------------------------------------------------------------------- 1 | interface Loopback100 2 | description ***MERGE LOOPBACK 100**** 3 | ip address 1.1.1.100 255.255.255.255 4 | interface Loopback200 5 | description ***MERGE LOOPBACK 200**** 6 | ip address 1.1.1.200 255.255.255.255 7 | end 8 | -------------------------------------------------------------------------------- /replace.conf: -------------------------------------------------------------------------------- 1 | telnet vrf default ipv4 server max-servers 10 2 | username vagrant 3 | group root-lr 4 | group cisco-support 5 | secret 5 $1$Vq36$trKjMVaD/QNTShlBIo9XX/ 6 | ! 7 | tpa 8 | address-family ipv4 9 | update-source MgmtEth0/RP0/CPU0/0 10 | ! 11 | ! 12 | interface Loopback100 13 | description ***REPLACE LOOPBACK 100**** 14 | ipv4 address 3.3.3.100 255.255.255.255 15 | ! 16 | interface Loopback200 17 | description ***REPLACE LOOPBACK 200**** 18 | ipv4 address 3.3.3.200 255.255.255.255 19 | ! 20 | interface MgmtEth0/RP0/CPU0/0 21 | ipv4 address 10.10.20.175 255.255.255.0 22 | ! 23 | ! 24 | interface MgmtEth0/RP0/CPU0/0 25 | ipv4 address dhcp 26 | ! 27 | interface preconfigure TenGigE0/0/0/0 28 | ! 29 | router static 30 | address-family ipv4 unicast 31 | 0.0.0.0/0 MgmtEth0/RP0/CPU0/0 10.0.2.2 32 | ! 33 | ! 34 | ssh server v2 35 | ssh server vrf default 36 | grpc 37 | port 57777 38 | ! 39 | xml agent tty iteration off 40 | ! 41 | end 42 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | napalm==2.5.0 2 | click==7.1.1 3 | -------------------------------------------------------------------------------- /rollback_loopbacks.cfg: -------------------------------------------------------------------------------- 1 | interface Loopback100 2 | description ***ROLLBACK LOOPBACK 100**** 3 | ip address 2.2.2.100 255.255.255.255 4 | interface Loopback200 5 | description ***ROLLBACK LOOPBACK 200**** 6 | ip address 2.2.2.200 255.255.255.255 7 | end 8 | -------------------------------------------------------------------------------- /validate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - get_facts: 3 | os_version: 6.1.2 4 | vendor: Cisco 5 | 6 | - get_interfaces: 7 | MgmtEth0/RP0/CPU0/0: 8 | is_up 9 | 10 | - get_interfaces_ip: 11 | MgmtEth0/RP0/CPU0/0: 12 | ipv4: 13 | 10.0.2.15: 14 | prefix_length: 24 15 | _mode: strict 16 | --------------------------------------------------------------------------------