├── README.md ├── coding-101-python ├── absolute_basics.py ├── virtual_environment.md ├── interactive_shell.md ├── control_flow.py ├── regex.py ├── arithmetics.py ├── import_system.py ├── pypi.md ├── functions_n_classes.py ├── basic_datatype.py └── more_advanced_topics.py ├── COPYRIGHT ├── coding-102-parsers ├── json.py ├── xml.py └── strings.py ├── coding-103-yaml ├── basics.yaml └── movie.yaml ├── .gitignore ├── coding-201-advanced-parsers ├── metaparser_schema.py └── metaparser_diff_contexts_example.py └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | # pyATS Coding 101 2 | 3 | Basic Python coding, examples & techniques for [Cisco DevNet - pyATS](https://developer.cisco.com/site/pyats) based test automation. 4 | 5 | The goal of this repository is to demonstrate fundamental skillsets related to: 6 | - basic Python techniques 7 | - CLI/XML parsing 8 | - expect-style programming 9 | - YAML syntax and files 10 | - connecting to devices 11 | - defining testbed topologies using pyATS 12 | - etc 13 | 14 | The content of this repository extends [Cisco DevNet - Coding Skills](https://github.com/CiscoDevNet/coding-skills-sample-code). 15 | 16 | *Note: Python-3 syntax, tested with Python 3.4+* 17 | -------------------------------------------------------------------------------- /coding-101-python/absolute_basics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | The Basics of Python Language (Python 3+) 5 | ----------------------------------------- 6 | 7 | - python program files ends with .py 8 | typically, you can run a python program file using: 9 | bash$ python .py 10 | 11 | - python files are white space sensitive 12 | eg: indentation == structure. 13 | do not mix spaces with tabs - stick with ONE. 14 | ''' 15 | 16 | # define a function 17 | def hello_world(): 18 | # as usuals :) 19 | print('hello world') 20 | 21 | 22 | # boilerplate code - the standard entrypoint to a python program 23 | if __name__ == '__main__': 24 | # call our function 25 | hello_world() -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------ 2 | # Copyright 2017 Cisco Systems 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ------------------------------------------------------------------ 16 | -------------------------------------------------------------------------------- /coding-101-python/virtual_environment.md: -------------------------------------------------------------------------------- 1 | # Python Virtual Environment 2 | 3 | A Python virtual environment is an isolated Python environment that allows you 4 | install various packages & modules into, without impacting anything else. 5 | 6 | A Python virtual environment appears as a standard file system directory. To 7 | create a new virtual environment, do: 8 | 9 | ```bash 10 | 11 | bash$ python3 -m venv my_new_venv 12 | ``` 13 | 14 | This will create a new virtual environment in a folder called `my_new_venv` 15 | under the current directory. 16 | 17 | To *set into and start using* this virtual environment, you **activate** it by: 18 | ```bash 19 | bash$ source my_new_venv/bin/activate # for bash 20 | csh> source my_new_venv/bin/activate.csh $ for csh 21 | ``` 22 | 23 | You can delete any virtual environment by just deleting the folder. This will 24 | destroy/delete all packages/content managed under this virtual environment. 25 | 26 | Read more on [Python venv](https://docs.python.org/3/tutorial/venv.html) 27 | -------------------------------------------------------------------------------- /coding-101-python/interactive_shell.md: -------------------------------------------------------------------------------- 1 | # Python Interactive Shell 2 | 3 | Python is an interpreted language. One of the best ways to learn Python is to 4 | drop into the interactive shell, type code in it, and see it get evaluated as 5 | you hit enter. No compilation necessary. 6 | 7 | To start, type python3 (since we are using python3) in your shell. 8 | ```bash 9 | bash$ python3 10 | 11 | Python 3.4.1 (default, Nov 12 2014, 13:34:48) 12 | [GCC 4.4.6 20120305 (Red Hat 4.4.6-4)] on linux 13 | Type "help", "copyright", "credits" or "license" for more information. 14 | ``` 15 | 16 | and type away: 17 | ```python 18 | >>> foo = "bar" 19 | >>> number = 40 + 2 20 | >>> print('hello world') 21 | hello world 22 | 23 | ``` 24 | 25 | There are some nice functions in the interactive shell: 26 | ```python 27 | >>> import sys # let's import a standard module 28 | >>> help(sys) # display help text w.r.t. module sys 29 | >>> dir(sys) # print the list of methods/attributes/properties 30 | # of the given object 31 | ``` -------------------------------------------------------------------------------- /coding-101-python/control_flow.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Conditionals 3 | ------------ 4 | if ... elif ... else 5 | 6 | Loops 7 | ----- 8 | for ... in ... 9 | while ... 10 | 11 | ''' 12 | 13 | # IF statement 14 | # ------------ 15 | 16 | # basic if statement 17 | if 40 + 2 == 42: 18 | print('The answer of everything is 42') 19 | 20 | number = 100 21 | if number == 100: 22 | print('number equal to 100') 23 | elif number > 100: 24 | print('number greater than 100') 25 | else: 26 | print('number smaller than 100') 27 | 28 | # FOR/WHILE loops 29 | # --------------- 30 | 31 | # for loop 32 | for character in 'abcdefghijklmnopqrstuvwxyz': 33 | print(character) 34 | 35 | # while loop 36 | numbers = [1, 2, 3, 4, 5] 37 | while numbers: 38 | print(numbers.pop()) 39 | 40 | # Control 41 | # ------- 42 | 43 | # loop control statements 44 | for character in 'abcdefghijklmnopqrstuvwxyz': 45 | if character == 'j': 46 | break 47 | elif character == 'e': 48 | continue 49 | print(character) 50 | 51 | numbers = [1, 2, 3, 4, 5] 52 | while numbers: 53 | if numbers == 4: 54 | break 55 | elif number == 2: 56 | continue 57 | print(numbers.pop()) -------------------------------------------------------------------------------- /coding-102-parsers/json.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Parsing JSON 3 | ------------ 4 | 5 | Python comes with a built-in module called 'json'. It contains the necessary 6 | methods and functionality to parse & build JSON strings. 7 | 8 | Read more at https://docs.python.org/3/library/json.html 9 | ''' 10 | 11 | # the built-in module to handle json 12 | import json 13 | 14 | JSON_STRING = ''' 15 | { 16 | "string": "hello world", 17 | "boolean": true, 18 | "null": null, 19 | "numbers": 1234567, 20 | "object": { 21 | "key": "value", 22 | "another_key": "another_value" 23 | }, 24 | "arrays": [ 25 | "item_1", 26 | "item_2", 27 | "item_3" 28 | ] 29 | } 30 | ''' 31 | 32 | PYTHON_DICT = { 33 | 'arrays': ['item_1', 'item_2', 'item_3'], 34 | 'boolean': True, 35 | 'null': None, 36 | 'numbers': 1234567, 37 | 'object': {'another_key': 'another_value', 'key': 'value'}, 38 | 'string': 'hello world' 39 | } 40 | 41 | # convert a json string to python object 42 | converted_object = json.loads(JSON_STRING) 43 | 44 | # the converted object is a dictionary 45 | print(converted_object["string"]) 46 | 47 | # you can also convert structures back from python to json 48 | converted_to_json = json.dumps(PYTHON_DICT) -------------------------------------------------------------------------------- /coding-103-yaml/basics.yaml: -------------------------------------------------------------------------------- 1 | # YAML Basic Syntax 2 | # ----------------- 3 | # 4 | # YAML syntax is white-space sensitive. 5 | 6 | # comments in YAML starts with hashtag 7 | # like this 8 | 9 | # typical structure: 10 | # key: value 11 | 12 | # scalar variables 13 | integer: 42 14 | string: "this is a string" 15 | float: 3.14 16 | boolean: true 17 | 18 | # lists can be defined using "-" on each newline 19 | books: 20 | - "Les Miserable" 21 | - "Pride & Prejudice" 22 | 23 | # or within [] 24 | names: ["John", "Joe", "Josh"] 25 | 26 | # mappings/dictionary 27 | character: 28 | callsign: blackwidow 29 | role: melee 30 | name: "natasha romanoff" 31 | skills: 32 | - "tactician" 33 | - "martial artist" 34 | - "marksman" 35 | 36 | # mapping/dictionary type can also be represented using {} 37 | character: {callsign: blackwidow, role: melee} 38 | 39 | # long strings, use | to preserve newlines and spaces 40 | long_string_with_newlines: | 41 | text in here will 42 | appear as it is typed 43 | and the newlines will be preserved, 44 | just like a haiku 45 | 46 | long_one_liner_string: > 47 | this is a very long string where, 48 | no matter how many lines I put it over, 49 | it will print out to be a single line, 50 | because with >, newlines are not 51 | preserved. -------------------------------------------------------------------------------- /coding-101-python/regex.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Regular Expression in Python 3 | ---------------------------- 4 | 5 | - use 're' module to do anything regular expression related 6 | - pre-compiling regex patterns will speed up execution 7 | - regex matching typically produces match objects 8 | - it's a good idea to use r'' (raw string literal) to define regex patterns 9 | ''' 10 | 11 | import re 12 | 13 | # basic string matching 14 | re.match(r'[0-9]+', '12345') 15 | 16 | # pre-compile to speedup the process 17 | pattern = re.compile(r'[0-9]+') 18 | pattern.match('12345') 19 | pattenr.match('100') 20 | 21 | # use if to test whether a match occured 22 | if pattern.match('12345'): 23 | print('pattern match!') 24 | else: 25 | print('no match!') 26 | 27 | # test if a string contains a pattern 28 | pattern = re.compile(r'([a-z]+)') 29 | if pattern.search("123hello456world789"): 30 | print('string contains characters') 31 | 32 | # find all occurances of a pattern in a string 33 | pattern = re.compile(r'([a-z]+)') 34 | for match in pattern.findall("123hello456world789"): 35 | print(match) 36 | 37 | # matching pattern into variables 38 | result = re.match(r"(?P\w+) (?P\w+)", "Natasha Romanoff") 39 | result.groupdict() 40 | {'first_name': 'Natasha', 'last_name': 'Romanoff'} -------------------------------------------------------------------------------- /coding-102-parsers/xml.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Parsing XML 3 | ----------- 4 | 5 | Python features a built-in module named 'xml' that facilitates the parsing and 6 | constructuring of XML. Alternatively, you could also use the PyPI 'lxml' 7 | package, a more powerful xml parser. 8 | 9 | For simplicity, this example uses only built-in python xml module. 10 | 11 | Read more at https://docs.python.org/3/library/xml.html 12 | http://lxml.de/ 13 | ''' 14 | 15 | import xml 16 | 17 | XML_STRING = ''' 18 | 19 | 20 | natasha 21 | romanoff 22 | 23 | 24 | tony 25 | stark 26 | 27 | 28 | ''' 29 | from xml.etree import ElementTree 30 | 31 | # parse into root 32 | root = ElementTree.fromstring(XML_STRING) 33 | 34 | root.tag 35 | # 'data' 36 | 37 | for child in root: 38 | print(child.tag, child.attrib) 39 | print(" ", child[0].tag, child[0].text) 40 | print(" ", child[1].tag, child[1].text) 41 | # character {'name': 'blackwidow'} 42 | # firstname natasha 43 | # lastname romanoff 44 | # character {'name': 'ironman'} 45 | # firstname tony 46 | # lastname stark 47 | 48 | -------------------------------------------------------------------------------- /coding-101-python/arithmetics.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Arithmetic Operators 3 | -------------------- 4 | 5 | + addition 6 | - subtraction 7 | * multiplication 8 | / division 9 | % modulus 10 | // floor division 11 | ** power 12 | 13 | Assignment Operators 14 | -------------------- 15 | 16 | = assignment 17 | += increment & assign 18 | -= decrement & assign 19 | *= multiply & assign 20 | /= divide & assign 21 | %= modulus & assign 22 | //= floor division & assign 23 | **= power & assign 24 | 25 | Relational Operators 26 | -------------------- 27 | 28 | == equals to 29 | != not equal to 30 | > greater than 31 | < less than 32 | >= greater than or equal to 33 | <= less than or equal to 34 | 35 | Boolean Operators 36 | ----------------- 37 | 38 | add, or, not 39 | 40 | Identiy 41 | ------- 42 | 43 | is 44 | 45 | Membership 46 | ---------- 47 | 48 | in 49 | 50 | ''' 51 | 52 | # basic math 53 | ten = 5 + 5 54 | four = 2 ** 2 55 | one = 5 % 2 56 | 57 | # assignments 58 | number = 0 59 | number += 1 # increment by 1 60 | number -= 1 # decrement by 1 61 | 62 | # relational operators results in a boolean result 63 | 0 > 1 # False 64 | 1 < 2 # True 65 | 3.14 >= 3 # True 66 | 67 | # boolean operators 68 | True and False # False 69 | True or False # True 70 | not True # false 71 | 72 | # identity check 73 | obj = None 74 | obj is None # True 75 | 76 | 77 | # membership 78 | 'a' in 'abcdefg' # True -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /coding-101-python/import_system.py: -------------------------------------------------------------------------------- 1 | ''' 2 | The Import System 3 | ----------------- 4 | 5 | there's two types of things that can be imported using the import statement 6 | - module 7 | - package (a module that can contain submodules) 8 | 9 | module: 10 | - is basically any file ending with .py 11 | - the code is executed on import 12 | 13 | package: 14 | - a directory containing __init__.py file 15 | - only the content in __init__.py file is executed when imported. 16 | (unless subsequent modules within this directory is imported in this file) 17 | 18 | absolute import in python3 19 | 20 | - only modules and packages directly reacheable in sys.path will be imported. 21 | 22 | sys.path 23 | - list of strings that specifies earch path for modules. 24 | - this is initiated from env var PYTHONPATH 25 | 26 | > it's considered very good practice to put all import statements on the < 27 | > top of your python file for readability. < 28 | 29 | https://www.python.org/dev/peps/pep-0328/ 30 | https://docs.python.org/3/reference/import.html 31 | ''' 32 | 33 | # typical imports 34 | import os 35 | import xml.parser 36 | import collections, functools, itertools 37 | 38 | from collections import OrderedDict 39 | from itertools import count, cycle, repeat 40 | 41 | # rename on import 42 | # (avoid local naming clash) 43 | from os.path import join as join_paths_together 44 | 45 | # you can span a single import over multiple lines 46 | from os.path import (abspath, basename, dirname, 47 | exists, expanduser, join, 48 | ismount, realpath, relpath, stat) 49 | 50 | # relative imports within a package 51 | # (the following are for illustrative only) 52 | from . import some_other_module 53 | from ..another_package import some_module 54 | 55 | # when an import occurs, the imported module (by name) is put 56 | # into your current module scope as the module object 57 | import sys 58 | print(sys.copyright) 59 | 60 | # modify sys.path to enable python to search more paths for python modules. 61 | import sys 62 | sys.path.append('/path/to/your/directory') 63 | import package_from_your_directory 64 | -------------------------------------------------------------------------------- /coding-101-python/pypi.md: -------------------------------------------------------------------------------- 1 | # Python Package Index 2 | 3 | The [Python Package Index](https://pypi.python.org/pypi), a.k.a. PyPI, is a 4 | public repository of openly available packages/softwares for the Python 5 | programming language. 6 | 7 | PyPI packages are managed & installed by `pip`, the Python package management 8 | system. `pip` is installed by default in Python virtual environments. 9 | 10 | ```bash 11 | bash$ pip list # list the packages currently installed 12 | pip (9.0.1) 13 | setuptools (36.7.2) 14 | ``` 15 | 16 | It's a good idea to search packages in [PyPI](https://pypi.python.org/pypi), 17 | and install it using `pip`: 18 | 19 | For example, to install [requests](https://pypi.python.org/pypi/requests) 20 | ```bash 21 | bash$ pip install requests 22 | Collecting requests 23 | Downloading requests-2.18.4-py2.py3-none-any.whl (88kB) 24 | 100% |████████████████████████████████| 92kB 932kB/s 25 | Collecting urllib3<1.23,>=1.21.1 (from requests) 26 | Using cached urllib3-1.22-py2.py3-none-any.whl 27 | Collecting chardet<3.1.0,>=3.0.2 (from requests) 28 | Using cached chardet-3.0.4-py2.py3-none-any.whl 29 | Collecting idna<2.7,>=2.5 (from requests) 30 | Using cached idna-2.6-py2.py3-none-any.whl 31 | Collecting certifi>=2017.4.17 (from requests) 32 | Downloading certifi-2017.11.5-py2.py3-none-any.whl (330kB) 33 | 100% |████████████████████████████████| 337kB 2.0MB/s 34 | Installing collected packages: urllib3, chardet, idna, certifi, requests 35 | Successfully installed certifi-2017.11.5 chardet-3.0.4 idna-2.6 requests-2.18.4 urllib3-1.22 36 | ``` 37 | 38 | `pip` automatically finds the package from PyPI, and installs it (and any of its 39 | dependencies) into your current virtual environment. 40 | 41 | Here's some useful commands: 42 | ```bash 43 | bash$ pip install requests==2.18.4 # install a particular version 44 | bash$ pip install --upgrade requests # upgrade the specified package 45 | bash$ pip freeze > requirements.txt # produce the list of all packages installed in this venv into file 46 | bash$ pip install -r requirements.txt # install all packages/version specified in file 47 | ``` 48 | 49 | Read more on `pip` at [PIP Documentation](https://pip.pypa.io/en/stable/) -------------------------------------------------------------------------------- /coding-103-yaml/movie.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- # movie release 3 | 4 | movie: Spectre 5 | date: 2015-11-06 6 | director: Sam Mendes 7 | writers: 8 | - Ian Fleming 9 | - John Logan 10 | - Neal Purvis 11 | - Robert Wade 12 | - Jez Butterworth 13 | 14 | ratings: 15 | rotten_tomatos: 63% 16 | imdb: 75% 17 | 18 | cast: 19 | - character: James Bond 20 | starred_by: Daniel Craig 21 | 22 | - character: Oberhauser 23 | starred_by: Christoph Waltz 24 | 25 | - character: Madeleine Swann 26 | starred_by: Lea Seydoux 27 | 28 | - character: M 29 | starred_by: Ralph Fienne 30 | 31 | - character: Q 32 | starred_by: Ben Whishaw 33 | 34 | - character: Moneypenny 35 | starred_by: Naomie Harris 36 | 37 | storyline: > 38 | A cryptic message from the past sends James Bond on a rogue mission to 39 | Mexico City and eventually Rome, where he meets Lucia, the beautiful and 40 | forbidden widow of an infamous criminal. Bond infiltrates a secret meeting 41 | and uncovers the existence of the sinister organisation known as SPECTRE. 42 | Meanwhile back in London, Max Denbigh, the new head of the Centre of 43 | National Security, questions Bond's actions and challenges the relevance of 44 | MI6 led by M. Bond covertly enlists Moneypenny and Q to help him seek 45 | out Madeleine Swann, the daughter of his old nemesis Mr White, who may 46 | hold the clue to untangling the web of SPECTRE. As the daughter of 47 | the assassin, she understands Bond in a way most others cannot. As Bond 48 | ventures towards the heart of SPECTRE, he learns a chilling connection 49 | between himself and the enemy he seeks. 50 | 51 | quotes: | 52 | Oberhauser: Why did you come? 53 | James Bond: I came here to kill you. 54 | Oberhauser: And I thought you came here to die. 55 | James Bond: Well, it's all a matter of perspective. 56 | ------- 57 | Lucia Sciarra: If you don't leave now, we'll die together. 58 | James Bond: I can think of worse ways to go. 59 | Lucia Sciarra: Then you're obviously crazy, Mr... 60 | James Bond: Bond. James Bond. 61 | ------- 62 | 63 | keywords: [spy, espionage, sequel, sinister] 64 | 65 | genres: [action, adventure, thriller] -------------------------------------------------------------------------------- /coding-101-python/functions_n_classes.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Functions 3 | --------- 4 | - define a function using 'def' 5 | - a function can have arbitrary of arguments (positional and keyword) 6 | - functions return None if no return statement is provided 7 | 8 | Classes 9 | ------- 10 | - created using 'class' statement. 11 | - __init__() is the entry point 12 | - the first argument to any method in a class should be 'self'. 13 | 14 | ''' 15 | 16 | # Functions & Arguments 17 | # --------------------- 18 | 19 | # define a function 20 | def adder(x, y): 21 | # functions can return 22 | return x + y 23 | 24 | # access using positional arguments 25 | print(adder(1, 2)) 26 | 27 | # access using keyword arguments 28 | print(adder(x=1, y=2)) 29 | 30 | # return statement breaks execution and returns from the function 31 | # scope immediately 32 | def tester(x, y): 33 | if x + 1 > y: 34 | return 'x + 1 is greater than y' 35 | else: 36 | return 'x + 1 is not greater than y' 37 | print('done') # this is never executed ! 38 | 39 | print(tester(1, 3)) 40 | 41 | # arbitrary number of positional arguments 42 | def sum_numbers_up(*args): 43 | total = 0 44 | for number in args: 45 | total += number 46 | return total 47 | 48 | # arbitrary number of keyword arguments 49 | def print_arguments(**kwargs): 50 | for key, value in kwargs.items(): 51 | print('%s = %s' % (key, value)) 52 | 53 | # Classes 54 | # ------- 55 | 56 | # define a class 57 | class Person(object): 58 | '''Person class docstring ''' 59 | 60 | def __init__(self, firstname, lastname): 61 | '''require arguments firstname & lastname''' 62 | self.firstname = firstname 63 | self.lastname = lastname 64 | 65 | def print_name(self): 66 | print('{firstname} {lastname}'.format(firstname = self.firstname, 67 | lastname = self.lastname)) 68 | 69 | # create an instance of a class 70 | ironman = Person(firstname = 'Tony', lastname = 'Stark') 71 | 72 | # call methods 73 | ironman.print_name() 74 | 75 | # access attributes 76 | print(ironman.firstname) 77 | 78 | # create a subclass by inhering a parent class 79 | class Character(Person): 80 | 81 | def __init__(self, firstname, lastname, nickname): 82 | # call parent class's methods using super() 83 | super().__init__(firstname, lastname) 84 | self.nickname = nickname 85 | 86 | def print_nickname(self): 87 | print(self.nickname) 88 | 89 | ironman = Character(firstname = 'Tony', 90 | lastname = 'Stark', 91 | nickname = 'ironman') 92 | 93 | ironman.print_name() 94 | ironman.print_nickname() -------------------------------------------------------------------------------- /coding-101-python/basic_datatype.py: -------------------------------------------------------------------------------- 1 | ''' 2 | basic datatypes 3 | --------------- 4 | bool -> boolean 5 | int -> integer 6 | float -> decimals/float 7 | str -> string 8 | dict -> dictionaries (aka. hash map in some languages) 9 | list -> lists (aka. arrays in some langauges) 10 | tuples -> basically lists, but immutable 11 | None -> special object, singleton that means, well, nothing. 12 | ''' 13 | 14 | # there's no variable declaration/initialization - just name as you go 15 | # variable names may contain A-Z, a-z, 0-9, _, but cannot start with a number. 16 | foo = 'bar' # set variable named foo to contain string 'bar' 17 | total = 1 * 2 * 3 # set variable named total to result of 1 x 2 x 3 (6) 18 | 19 | # Booleans 20 | # -------- 21 | 22 | # there's true... and there's false 23 | truth = True 24 | falsehood = False 25 | 26 | # Integers/Floats 27 | # --------------- 28 | 29 | # integers are just numbers 30 | integer_1 = 1 31 | the_ultimate_answer_to_question_of_life_universe_and_everything = 42 32 | 33 | # declare a float by just using . naturally in math 34 | pi = 3.1415926535897932384626433832795028841971693 35 | 36 | # Strings 37 | # ------- 38 | 39 | # a string is typically surrounded by '', "" 40 | foo1 = "bar" 41 | foo2 = 'bar' # no difference 42 | 43 | # a '' quoted string cannot contain ' 44 | # a "" quoted string cannot contain " 45 | # (unless you escape it) 46 | foo3 = "\"bar\"" 47 | foo4 = '\rbar\'' 48 | 49 | # so it's better to mix them 50 | foo5 = '"bar"' 51 | foo6 = "'bar'" 52 | 53 | # a string must finish on the same line, unless you use \ at the end 54 | foo7 = "a multiple\ 55 | line string" 56 | 57 | # alternatively, use ''' and """ to denote multi-line strings without having 58 | # any of the above difficulties 59 | foo8 = '''python is a simple 60 | yet complicated programming 61 | language for "adults"''' 62 | 63 | # use index to access string position 64 | print(foo1[0]) 65 | 66 | # and slicing to indicate a sub-string 67 | print(foo7[2:10]) 68 | 69 | # there are many useful, built-in string methods 70 | 'python'.title() # Python 71 | 'uppercase'.upper() # UPPERCASE 72 | 'LOWERCASE'.lower() # lowercase 73 | 'a b c'.split() # ['a', 'b', 'c'] 74 | ' a good day '.strip() # 'a good day' 75 | 76 | # concatenate strings by adding them 77 | ironman = "Tony" + " " + "Stark" 78 | 79 | # Dictionary 80 | # ---------- 81 | 82 | # a dictionary comprises of key-value pairs 83 | # dictionaries are unordered (order of insertion is lost) 84 | character = {} 85 | character['firstname'] = "tony" 86 | character['lastname'] = "stark" 87 | character['nickname'] = "ironman" 88 | 89 | # alternatively, create it in one shot 90 | character = {'firstname': 'tony', 'lastname': 'stark', 'nickname': 'ironman'} 91 | 92 | # some basic methods of a dictionary 93 | print(character['firstname'], character['lastname']) 94 | charater.pop('firstname') 95 | charater.setdefault('home_automation', 'J.A.R.V.I.S') 96 | character.update(firstname = "Tony", lastname = "Stark") 97 | all_keys = character.keys() 98 | all_values = character.values() 99 | key_value_pairs = character.items() 100 | 101 | # Lists & Tuples 102 | # -------------- 103 | 104 | # lists are extremly versatile, and can contain anything (including other lists) 105 | # lists are ordered. 106 | movies = ['A New Hope', 'The Empire Strikes Back', 'Return of the Jedi'] 107 | combo = ["Arthur Dent", 42, list1] 108 | 109 | # lists are indexed by integer position starting from 0 110 | # negative numbers starts from the end of the list (starting from -1) 111 | print(movies[0], movies[1], movies[2]) 112 | print(movies[-1], movies[-2], movies[-3]) 113 | 114 | # typical list operations 115 | movies.append("The Phantom Menace") 116 | movies.extend(["Attack of the Clones", "Revenge of the Sith"]) 117 | movies[0] = "Episode IV - A New Hope" 118 | combo.pop(-1) # -1 means last item 119 | 120 | # slicing worsk too 121 | movies[3:] # everything from 3 onwards 122 | 123 | # tuples are the exact same as lists, but you cannot modify it. 124 | movies = ('A New Hope', 'The Empire Strikes Back', 'Return of the Jedi') 125 | 126 | # None 127 | # ---- 128 | 129 | # None is special 130 | nothing = None 131 | 132 | # Typecasting 133 | # ----------- 134 | 135 | # many things can be casted around 136 | number = "42" # starting with a string 137 | number = int(number) # cast to integer 138 | number = str(number) # casted back to string 139 | 140 | languages = ['Python', 'C#', 'Ruby'] # start with a list 141 | languages = tuple(languages) # cast to tuple -------------------------------------------------------------------------------- /coding-101-python/more_advanced_topics.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Exceptions 3 | ---------- 4 | 5 | - try ... except ... 6 | - exception types 7 | 8 | Set/Get Attribute By Name 9 | ------------------------- 10 | 11 | - setattr 12 | - getattr 13 | 14 | Advanced Classes 15 | ---------------- 16 | 17 | - classmethods 18 | - staticmethods 19 | - class variable vs instance variable 20 | - properties 21 | 22 | Context Manager 23 | --------------- 24 | 25 | - with statements 26 | 27 | 28 | Decorators 29 | ---------- 30 | 31 | - what is a decorator 32 | - how to write a simple decorator 33 | 34 | ''' 35 | 36 | # Exception Handling 37 | # ------------------ 38 | 39 | # to catch a python exception & handle it, use try/except 40 | try: 41 | 1 / 0 42 | except ZeroDivisionError: 43 | print('Cannot divide a number by zero!') 44 | 45 | # you can also handle the exception... and do something in a cleanup clause 46 | try: 47 | # use raise statment to raise an exception 48 | raise TypeError('This is a general TypeError') 49 | except TypeError as e: 50 | print('I caught the following %s' % e) 51 | finally: 52 | print('Print ... cleaning up') 53 | 54 | # always specify the exception type you are catching for 55 | # if you want to globally catch "any" exception, use the base class Exception 56 | try: 57 | {}['non_existent_key'] 58 | except KeyError: 59 | # handle the key error 60 | except Exception: 61 | # handle any other possible exceptions 62 | 63 | # it's almost never a good idea to do the following: 64 | try: 65 | # generate an exception 66 | raise Exception() 67 | except: # not specifying exception type 68 | # handle exception 69 | # because this will catch ALL exceptions, including the few that derive from 70 | # BaseException, including SystemExit, KeyboardInterrupt... etc. 71 | # for more details, see: https://docs.python.org/3/library/exceptions.html#exception-hierarchy 72 | 73 | # Set/Get Attribute By Name 74 | # ------------------------- 75 | 76 | # sometimes you may want to set/get attributes by name 77 | # do that with setattr() and getattr() 78 | class MyObject(object): pass 79 | obj = MyObject() 80 | setattr(obj, 'new_attribute', 100) 81 | getattr(obj, 'new_attribute') 82 | 83 | 84 | # Advanced Classes 85 | # ---------------- 86 | 87 | # classes may contain classmethods, staticmethods, and class variables 88 | # in addition to standard methods: 89 | 90 | class Fruits(object): 91 | 92 | # a class variable common to all instances of Fruits 93 | all_fruits = [] 94 | 95 | def __init__(self, name): 96 | self.name = name 97 | 98 | # add this fruit instance to the list of all fruits. 99 | self.all_fruits.append(self) 100 | 101 | @classmethod 102 | def print_all_fruits(cls): 103 | ''' 104 | a class method is declared using the @classmethod decorator, and 105 | conventionally receives the class it is defined under as the first 106 | argument 'cls' 107 | ''' 108 | print([fruit.name for fruit in cls.all_fruits]) 109 | 110 | @staticmethod 111 | def print_message(): 112 | ''' 113 | static methods are those that got decoreated with @staticmethod, and 114 | do not receive a class instance or the class as argument. 115 | ''' 116 | print('i am a fruit!') 117 | 118 | @property 119 | def capitalized_name(self): 120 | ''' 121 | define a property attribute using @property decorator. a property 122 | attribute is a 'function' that gets the attribute value. 123 | 124 | (property is the setter/getter functionality equivalent in Python) 125 | 126 | For more information, refer to https://docs.python.org/3/library/functions.html#property 127 | 128 | ''' 129 | return self.name.title() 130 | 131 | # Context Manager 132 | # --------------- 133 | 134 | # python context managers are used in conjunction with the 'with' statement. 135 | # they are typically used to manage resources that are 'scarce' by automatically 136 | # providing closure when leaving the given context (Sheldon would love it) 137 | 138 | # consider the following 139 | file = open('somefile.txt') 140 | # ... 141 | file.close() 142 | 143 | # what if the above code in ... failed/raised exception? what if we forgot to 144 | # call file.close()? the resource/filehandler would remain open until process 145 | # exit... unless we always wrapped it in try/except clauses and were very 146 | # careful with our code. This is prone to human error. 147 | 148 | # with to the rescue: 149 | with open('somefile.txt') as file: 150 | # ... 151 | 152 | # when the code finishes execution and leaves the context of with statement, 153 | # file.close() will automatically be called by the context manager/with. 154 | 155 | # for more details on how to write context managers, refer to: 156 | # https://docs.python.org/3/reference/compound_stmts.html#with 157 | # https://docs.python.org/3/library/contextlib.html 158 | 159 | 160 | # Decorators 161 | # ---------- 162 | # 163 | # a decorator is a function that takes another function and extends its 164 | # behavior without modifying it 165 | # 166 | # decorator code only ever run one: at import time 167 | 168 | def my_decorator(func): 169 | def wrapper(): 170 | print("Something is happening before func() is called.") 171 | func() 172 | print("Something is happening after func() is called.") 173 | return wrapper 174 | 175 | @my_decorator 176 | def a(): 177 | print('a got called') 178 | -------------------------------------------------------------------------------- /coding-102-parsers/strings.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Parsing Strings 3 | --------------- 4 | 5 | Parsing strings/long text (also known as screen scraping) is an essential 6 | skillset when dealing with CLI output. The most typical method of parsing 7 | text is to use regular expressions. 8 | 9 | ''' 10 | 11 | import re 12 | import pprint 13 | 14 | 15 | STRING = ''' 16 | Neighbor: 21.0.0.1, Address-Family: ipv4 Unicast (VRF1) 17 | Locally configured policies: 18 | route-map RMAP in 19 | route-map RMAP2 out 20 | Neighbor: 21.0.0.2, Address-Family: ipv6 Unicast (VRF2) 21 | Locally configured policies: 22 | route-map RMAP3 in 23 | route-map RMAP4 out 24 | Neighbor: 21.0.0.3, Address-Family: VPNv4 Unicast 25 | Locally configured policies: 26 | route-map RMAP5 in 27 | route-map RMAP6 out 28 | Neighbor: 21.0.0.4, Address-Family: VPNv6 Unicast (VRF2) 29 | Locally configured policies: 30 | route-map RMAP7 in 31 | route-map RMAP8 out 32 | ''' 33 | 34 | # initialize dictionary 35 | PYTHON_DICT = {} 36 | 37 | # split the output into seperate lines 38 | for line in STRING.splitlines(): 39 | # strip the line from any trailing white spaces 40 | line = line.rstrip() 41 | 42 | # regexp to parse the below different outputs 43 | # ** Neighbor: 21.0.0.1, Address-Family: ipv4 Unicast 44 | # ** Neighbor: 21.0.0.2, Address-Family: VPNv4 Unicast (VRF2) 45 | p1 = re.compile(r'^\s*Neighbor: +(?P[0-9\.]+),' 46 | ' +Address-Family:' 47 | ' +(?P[a-zA-Z0-9\s]+)' 48 | '( +\((?P[a-zA-Z0-9]+)\))?$') 49 | # check if the regexp matched with the line 50 | m = p1.match(line) 51 | if m: 52 | # gather the prased keys 53 | neighbor_name = m.groupdict()['neighbor_name'] 54 | address_family_name = m.groupdict()['address_family_name'] 55 | # check if vrf has been identified in the output, if not default 56 | # it to 'default' 57 | if m.groupdict()['vrf_name']: 58 | vrf_name = m.groupdict()['vrf_name'] 59 | else: 60 | vrf_name = 'default' 61 | continue 62 | 63 | # regexp to parse the below outputs 64 | # ** route-map RMAP in 65 | # ** route-map RMAP out 66 | p2 = re.compile(r'\s*route-map +(?P[a-zA-Z0-9]+) ' 67 | '+(?P[a-z]+)') 68 | # check if the regexp matched with the line 69 | m = p2.match(line) 70 | if m: 71 | # gather the prased keys 72 | route_map_name = m.groupdict()['route_map_name'] 73 | direction = m.groupdict()['direction'] 74 | 75 | # build the desired structure/hierarchy out of the parsed output 76 | if 'vrf' not in PYTHON_DICT: 77 | PYTHON_DICT['vrf'] = {} 78 | if vrf_name not in PYTHON_DICT['vrf']: 79 | PYTHON_DICT['vrf'][vrf_name] = {} 80 | if 'neighbor' not in PYTHON_DICT['vrf'][vrf_name]: 81 | PYTHON_DICT['vrf'][vrf_name]['neighbor'] = {} 82 | if neighbor_name not in PYTHON_DICT['vrf'][vrf_name]['neighbor']: 83 | PYTHON_DICT['vrf'][vrf_name]['neighbor'][neighbor_name] = {} 84 | if 'address_family' not in PYTHON_DICT['vrf'][vrf_name]['neighbor']\ 85 | [neighbor_name]: 86 | PYTHON_DICT['vrf'][vrf_name]['neighbor'][neighbor_name]\ 87 | ['address_family'] = {} 88 | if address_family_name not in PYTHON_DICT['vrf'][vrf_name]['neighbor']\ 89 | [neighbor_name]['address_family']: 90 | PYTHON_DICT['vrf'][vrf_name]['neighbor'][neighbor_name]\ 91 | ['address_family'][address_family_name] = {} 92 | 93 | if direction == 'in': 94 | PYTHON_DICT['vrf'][vrf_name]['neighbor'][neighbor_name]\ 95 | ['address_family'][address_family_name]['route_map_in'] = \ 96 | route_map_name 97 | else: 98 | PYTHON_DICT['vrf'][vrf_name]['neighbor'][neighbor_name]\ 99 | ['address_family'][address_family_name]['route_map_out'] = \ 100 | route_map_name 101 | 102 | continue 103 | 104 | # print the parsed built structure 105 | pprint.pprint(PYTHON_DICT) 106 | 107 | # Expected output 108 | # =============== 109 | 110 | # PYTHON_DICT = { 111 | # 'vrf': 112 | # {'VRF1': 113 | # {'neighbor': 114 | # {'21.0.0.1': 115 | # {'address_family': 116 | # {'VPNv4 Unicast': 117 | # {'route_map_in': 'RMAP', 118 | # 'route_map_out': 'RMAP2'} 119 | # } 120 | # } 121 | # } 122 | # }, 123 | # 'VRF2': 124 | # {'neighbor': 125 | # {'21.0.0.2': 126 | # {'address_family': 127 | # {'VPNv6 Unicast': 128 | # {'route_map_in': 'RMAP3', 129 | # 'route_map_out': 'RMAP4'} 130 | # } 131 | # }, 132 | # '21.0.0.4': 133 | # {'address_family': 134 | # {'VPNv6 Unicast': 135 | # {'route_map_in': 'RMAP7', 136 | # 'route_map_out': 'RMAP8'} 137 | # } 138 | # } 139 | # } 140 | # }, 141 | # 'default': 142 | # {'neighbor': 143 | # {'21.0.0.3': 144 | # {'address_family': 145 | # {'VPNv4 Unicast': 146 | # {'route_map_in': 'RMAP5', 147 | # 'route_map_out': 'RMAP6'} 148 | # } 149 | # } 150 | # } 151 | # } 152 | # } 153 | # } -------------------------------------------------------------------------------- /coding-201-advanced-parsers/metaparser_schema.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Metaparser 3 | ---------- 4 | 5 | Metaparser pyats module provides a structure unification among different 6 | parsing contexts. It provides a unified structure that will be followed 7 | by different device connections (Ex: 'cli', 'yang', 'xml). 8 | 9 | It addresses the common automation problems such as: 10 | ---------------------------------------------------- 11 | 12 | * Script changes due to CLI command and/or output changes 13 | (release to release, OS to OS, Platform/Series to Platform/Series) 14 | * Parser reuse limitation due to naming convention 15 | 16 | ''' 17 | 18 | # Python 19 | import re 20 | import pprint 21 | 22 | # Metaparser 23 | from metaparser import MetaParser 24 | # Schema is the user datastructure that every output from the inheriting class 25 | # should comply with. 26 | # * Any : Marks a section of a schema that matches anything. 27 | # * Optional : Marks an optional part of the schema. 28 | # * OR : Defines a schema of OR relationship, eg, the input data must pass 29 | # the validation of one of the requirements of this Schema. 30 | from metaparser.util.schemaengine import Schema, Any, Optional, Or 31 | 32 | 33 | class ParserSchema(MetaParser): 34 | # Schema could be any valid python datastructure/callable. 35 | schema = { 36 | 'vrf': 37 | {Any(): 38 | {'neighbor': 39 | {Any(): 40 | {'address_family': 41 | {Any(): 42 | # Optional here means that key is not a mandatory 43 | # key in the parsed output. 44 | {Optional('route_map_in'): str, 45 | # OR here means that parsed key could be either 46 | # of type(str) or type(list). 47 | 'route_map_out': Or(str, list), 48 | } 49 | }, 50 | } 51 | }, 52 | } 53 | }, 54 | } 55 | 56 | 57 | class Parser(ParserSchema): 58 | 59 | STRING = ''' 60 | Neighbor: 21.0.0.1, Address-Family: ipv4 Unicast (VRF1) 61 | Locally configured policies: 62 | route-map RMAP in 63 | route-map RMAP2 out 64 | Neighbor: 21.0.0.2, Address-Family: ipv6 Unicast (VRF2) 65 | Locally configured policies: 66 | route-map RMAP3 in 67 | route-map RMAP4 out 68 | Neighbor: 21.0.0.3, Address-Family: VPNv4 Unicast 69 | Locally configured policies: 70 | route-map RMAP5 in 71 | route-map RMAP6 out 72 | Neighbor: 21.0.0.4, Address-Family: VPNv6 Unicast (VRF2) 73 | Locally configured policies: 74 | route-map RMAP7 in 75 | route-map RMAP8 out 76 | ''' 77 | 78 | def cli(self): 79 | 80 | # Initialize dictionary 81 | result = {} 82 | 83 | # split the output into seperate lines 84 | for line in self.STRING.splitlines(): 85 | # strip the line from any trailing white spaces 86 | line = line.rstrip() 87 | 88 | # regexp to parse the below different outputs 89 | # ** Neighbor: 21.0.0.1, Address-Family: ipv4 Unicast 90 | # ** Neighbor: 21.0.0.2, Address-Family: VPNv4 Unicast (VRF2) 91 | p1 = re.compile(r'^\s*Neighbor: +(?P[0-9\.]+),' 92 | ' +Address-Family:' 93 | ' +(?P[a-zA-Z0-9\s]+)' 94 | '( +\((?P[a-zA-Z0-9]+)\))?$') 95 | # check if the regexp matched with the line 96 | m = p1.match(line) 97 | if m: 98 | # gather the prased keys 99 | neighbor_name = m.groupdict()['neighbor_name'] 100 | address_family_name = m.groupdict()['address_family_name'] 101 | # check if vrf has been identified in the output, if not default 102 | # it to 'default' 103 | if m.groupdict()['vrf_name']: 104 | vrf_name = m.groupdict()['vrf_name'] 105 | else: 106 | vrf_name = 'default' 107 | continue 108 | 109 | # regexp to parse the below outputs 110 | # ** route-map RMAP in 111 | # ** route-map RMAP out 112 | p2 = re.compile(r'\s*route-map +(?P[a-zA-Z0-9]+) ' 113 | '+(?P[a-z]+)') 114 | m = p2.match(line) 115 | # check if the regexp matched with the line 116 | if m: 117 | # gather the prased keys 118 | route_map_name = m.groupdict()['route_map_name'] 119 | direction = m.groupdict()['direction'] 120 | 121 | # build the desired structure/hierarchy out of the parsed output 122 | if 'vrf' not in result: 123 | result['vrf'] = {} 124 | if vrf_name not in result['vrf']: 125 | result['vrf'][vrf_name] = {} 126 | if 'neighbor' not in result['vrf'][vrf_name]: 127 | result['vrf'][vrf_name]['neighbor'] = {} 128 | if neighbor_name not in result['vrf'][vrf_name]\ 129 | ['neighbor']: 130 | result['vrf'][vrf_name]['neighbor'][neighbor_name] = {} 131 | if 'address_family' not in result['vrf'][vrf_name]\ 132 | ['neighbor'][neighbor_name]: 133 | result['vrf'][vrf_name]['neighbor'][neighbor_name]\ 134 | ['address_family'] = {} 135 | if address_family_name not in result['vrf'][vrf_name]\ 136 | ['neighbor'][neighbor_name]['address_family']: 137 | result['vrf'][vrf_name]['neighbor'][neighbor_name]\ 138 | ['address_family'][address_family_name] = {} 139 | 140 | if direction == 'in': 141 | result['vrf'][vrf_name]['neighbor'][neighbor_name]\ 142 | ['address_family'][address_family_name]\ 143 | ['route_map_in'] = route_map_name 144 | else: 145 | result['vrf'][vrf_name]['neighbor'][neighbor_name]\ 146 | ['address_family'][address_family_name]\ 147 | ['route_map_out'] = route_map_name 148 | 149 | continue 150 | 151 | # print the parsed built structure 152 | pprint.pprint(result) 153 | 154 | 155 | # Instantiate the parser class to run it 156 | My_object = Parser(device=None) 157 | 158 | # Call the cli function of the class to print the parsed output 159 | My_object.cli() 160 | 161 | 162 | # Parsed Output 163 | # ============= 164 | # result = { 165 | # 'vrf': 166 | # {'VRF1': 167 | # {'neighbor': 168 | # {'21.0.0.1': 169 | # {'address_family': 170 | # {'VPNv4 Unicast': 171 | # {'route_map_in': 'RMAP', 172 | # 'route_map_out': 'RMAP2'} 173 | # } 174 | # } 175 | # } 176 | # }, 177 | # 'VRF2': 178 | # {'neighbor': 179 | # {'21.0.0.2': 180 | # {'address_family': 181 | # {'VPNv6 Unicast': 182 | # {'route_map_in': 'RMAP3', 183 | # 'route_map_out': 'RMAP4'} 184 | # } 185 | # }, 186 | # '21.0.0.4': 187 | # {'address_family': 188 | # {'VPNv6 Unicast': 189 | # {'route_map_in': 'RMAP7', 190 | # 'route_map_out': 'RMAP8'} 191 | # } 192 | # } 193 | # } 194 | # }, 195 | # 'default': 196 | # {'neighbor': 197 | # {'21.0.0.3': 198 | # {'address_family': 199 | # {'VPNv4 Unicast': 200 | # {'route_map_in': 'RMAP5', 201 | # 'route_map_out': 'RMAP6'} 202 | # } 203 | # } 204 | # } 205 | # } 206 | # } 207 | # } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Cisco System, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /coding-201-advanced-parsers/metaparser_diff_contexts_example.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Metaparser with different contexts example 3 | ------------------------------------------ 4 | The below example will show the importance of having a common unified 5 | parser structure among different contexts. 6 | 7 | ** Both contexts (cli and xml) should comply with the schema defined in 8 | the inherited class 'ParserSchema'. 9 | ''' 10 | 11 | # Python 12 | import re 13 | import pprint 14 | import xml.etree.ElementTree as ET 15 | 16 | # Metaparser 17 | from metaparser import MetaParser 18 | # Schema is the user datastructure that every output from the inheriting class 19 | # should comply with. 20 | # * Any : Marks a section of a schema that matches anything. 21 | # * Optional : Marks an optional part of the schema. 22 | # * OR : Defines a schema of OR relationship, eg, the input data must pass 23 | # the validation of one of the requirements of this Schema. 24 | from metaparser.util.schemaengine import Schema, Any, Optional, Or 25 | 26 | # parser utils 27 | # ** This is needed to compare the cli created from the xml tags is matching 28 | # ** with the cli used in the cli function. 29 | from parser.utils.common import Common 30 | 31 | 32 | class ParserSchema(MetaParser): 33 | 34 | schema = { 35 | 'vrf': { 36 | Any(): { 37 | 'rpm_handle_count': int, 38 | Optional('route_map'): { 39 | Any():{ 40 | Any(): { 41 | 'action': str, 42 | 'seq_num': int, 43 | 'total_accept_count': int, 44 | 'total_reject_count': int, 45 | Optional('command'): { 46 | 'compare_count': int, 47 | 'match_count': int, 48 | 'command': str 49 | } 50 | }, 51 | }, 52 | } 53 | }, 54 | } 55 | } 56 | 57 | 58 | class Parser(ParserSchema): 59 | 60 | show_command = 'show bgp vrf all ipv4 unicast policy statistics redistribute' 61 | 62 | cli_input = ''' 63 | Details for VRF default 64 | Total count for redistribute rpm handles: 1 65 | 66 | C: No. of comparisions, M: No. of matches 67 | 68 | route-map ADD_RT_400_400 permit 10 69 | 70 | Total accept count for policy: 0 71 | Total reject count for policy: 0 72 | Details for VRF ac 73 | BGP policy statistics not available 74 | Details for VRF vpn1 75 | Total count for redistribute rpm handles: 1 76 | 77 | C: No. of comparisions, M: No. of matches 78 | 79 | route-map PERMIT_ALL_RM permit 20 80 | 81 | Total accept count for policy: 0 82 | Total reject count for policy: 0 83 | Details for VRF vpn2 84 | BGP policy statistics not available 85 | 86 | ''' 87 | 88 | xml_input = ''' 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | <__readonly__> 101 | 102 | 103 | default 104 | 1 105 | 106 | 107 | ADD_RT_400_400 108 | permit 109 | 10 110 | 0 111 | 0 112 | 113 | 114 | 115 | 116 | ac 117 | 0 118 | 119 | 120 | vpn1 121 | 1 122 | 123 | 124 | PERMIT_ALL_RM 125 | permit 126 | 20 127 | 0 128 | 0 129 | 130 | 131 | 132 | 133 | vpn2 134 | 0 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | ]]>]]> 150 | ''' 151 | 152 | def cli(self): 153 | 154 | # Init vars 155 | cli_parsed_output = {} 156 | index = 1 157 | vrf = 'default' 158 | 159 | for line in self.cli_input.splitlines(): 160 | line = line.strip() 161 | 162 | # Details for VRF default 163 | p1 = re.compile(r'^Details +for +VRF +' 164 | '(?P[\w\-]+)$') 165 | m = p1.match(line) 166 | if m: 167 | vrf = m.groupdict()['vrf'] 168 | nei_flag = True 169 | continue 170 | 171 | # No such neighbor 172 | if re.compile(r'No +such +neighbor$').match(line): 173 | nei_flag = False 174 | 175 | # regexp to parse the below different outputs 176 | # ** Total count for redistribute rpm handles: 1 177 | # ** Total count for neighbor rpm handles: 1 178 | # ** Total count for dampening rpm handles: 1 179 | p2 = re.compile(r'^Total +count +for +(?P\w+) +rpm +handles: +' 180 | '(?P\d+)$') 181 | m = p2.match(line) 182 | 183 | # regexp to parse the below output 184 | # ** BGP policy statistics not available 185 | p3 = re.compile(r'^BGP +policy +statistics +not +available$') 186 | m1 = p3.match(line) 187 | 188 | if m or m1: 189 | if 'vrf' not in cli_parsed_output: 190 | cli_parsed_output['vrf'] = {} 191 | 192 | if vrf not in cli_parsed_output['vrf']: 193 | cli_parsed_output['vrf'][vrf] = {} 194 | 195 | cli_parsed_output['vrf'][vrf]['rpm_handle_count'] = \ 196 | int(m.groupdict()['handles']) if m else 0 197 | continue 198 | 199 | # regexp to parse the below different outputs 200 | # ** route-map Filter-pip deny 10 201 | # ** route-map ADD_RT_400_400 permit 10 202 | # ** route-map RMAP_DIRECT->BGP_IPV4 permit 10 203 | p4 = re.compile(r'^route\-map +(?P\S+) +' 204 | '(?P\w+) +(?P\d+)$') 205 | m = p4.match(line) 206 | if m: 207 | name = m.groupdict()['name'] 208 | 209 | if 'route_map' not in cli_parsed_output['vrf'][vrf]: 210 | cli_parsed_output['vrf'][vrf]['route_map'] = {} 211 | 212 | if name not in cli_parsed_output['vrf'][vrf]['route_map']: 213 | cli_parsed_output['vrf'][vrf]['route_map'][name] = {} 214 | index = 1 215 | else: 216 | index += 1 217 | 218 | if index not in cli_parsed_output['vrf'][vrf]['route_map'][name]: 219 | cli_parsed_output['vrf'][vrf]['route_map'][name][index] = {} 220 | 221 | cli_parsed_output['vrf'][vrf]['route_map'][name][index]\ 222 | ['action'] = m.groupdict()['action'] 223 | 224 | cli_parsed_output['vrf'][vrf]['route_map'][name][index]\ 225 | ['seq_num'] = int(m.groupdict()['seqnum']) 226 | continue 227 | 228 | # regexp to parse the below different outputs 229 | # ** match ip address prefix-list pip-prefix C: 0 M: 0 230 | # ** match ip address prefix-list DIRECT->BGP_IPV4 C: 16 M: 0 231 | p5 = re.compile(r'^(?P[\w\s\-\>]+) +' 232 | 'C: +(?P\d+) +' 233 | 'M: +(?P\d+)$') 234 | m = p5.match(line) 235 | if m: 236 | command = m.groupdict()['command'].strip() 237 | 238 | if 'command' not in cli_parsed_output['vrf'][vrf]['route_map']\ 239 | [name][index]: 240 | cli_parsed_output['vrf'][vrf]['route_map'][name][index]\ 241 | ['command'] = {} 242 | 243 | cli_parsed_output['vrf'][vrf]['route_map'][name][index]\ 244 | ['command']['compare_count'] = int(m.groupdict() 245 | ['compare_count']) 246 | 247 | cli_parsed_output['vrf'][vrf]['route_map'][name][index]\ 248 | ['command']['match_count'] = \ 249 | int(m.groupdict()['match_count']) 250 | 251 | cli_parsed_output['vrf'][vrf]['route_map'][name][index]\ 252 | ['command']['command'] = command 253 | continue 254 | 255 | # regexp to parse the below different outputs 256 | # ** Total accept count for policy: 0 257 | p6 = re.compile(r'^Total +accept +count +for +policy: +' 258 | '(?P\d+)$') 259 | m = p6.match(line) 260 | if m: 261 | cli_parsed_output['vrf'][vrf]['route_map'][name][index]\ 262 | ['total_accept_count'] = int(m.groupdict() 263 | ['total_accept_count']) 264 | continue 265 | 266 | # regexp to parse the below different outputs 267 | # ** Total reject count for policy: 0 268 | p7 = re.compile(r'^Total +reject +count +for +policy: +' 269 | '(?P\d+)$') 270 | m = p7.match(line) 271 | if m: 272 | cli_parsed_output['vrf'][vrf]['route_map'][name][index]\ 273 | ['total_reject_count'] = int(m.groupdict() 274 | ['total_reject_count']) 275 | continue 276 | 277 | pprint.pprint(cli_parsed_output) 278 | 279 | 280 | def xml(self): 281 | 282 | xml_parsed_output = {} 283 | neighbor = None 284 | 285 | # Remove junk characters returned by the device 286 | out = self.xml_input.replace("]]>]]>", "") 287 | root = ET.fromstring(out) 288 | 289 | # top table root 290 | show_root = Common.retrieve_xml_child(root=root, key='show') 291 | # get xml namespace 292 | # {http://www.cisco.com/nxos:7.0.3.I7.1.:bgp} 293 | try: 294 | m = re.compile(r'(?P\{[\S]+\})').match(show_root.tag) 295 | namespace = m.groupdict()['name'] 296 | except: 297 | return xml_parsed_output 298 | 299 | # compare cli command 300 | Common.compose_compare_command(root=root, namespace=namespace, 301 | expect_command=self.show_command) 302 | 303 | # get neighbor 304 | nei = Common.retrieve_xml_child(root=root, 305 | key='__XML__PARAM__neighbor-id') 306 | 307 | if hasattr(nei, 'tag'): 308 | for item in nei.getchildren(): 309 | if '__XML__value' in item.tag: 310 | neighbor = item.text 311 | continue 312 | 313 | # cover the senario that __readonly__ may be missing when 314 | # there are values in the output 315 | if '__readonly__' in item.tag: 316 | root = item.getchildren()[0] 317 | else: 318 | root = item 319 | else: 320 | # top table rootl 321 | root = Common.retrieve_xml_child(root=root, key='TABLE_vrf') 322 | if not root: 323 | return xml_parsed_output 324 | 325 | # ----- loop vrf ----- 326 | for vrf_tree in root.findall('{}ROW_vrf'.format(namespace)): 327 | # vrf 328 | try: 329 | vrf = vrf_tree.find('{}vrf-name-polstats'.\ 330 | format(namespace)).text 331 | except: 332 | break 333 | 334 | if 'vrf' not in xml_parsed_output: 335 | xml_parsed_output['vrf'] = {} 336 | if vrf not in xml_parsed_output['vrf']: 337 | xml_parsed_output['vrf'][vrf] = {} 338 | 339 | # 1 340 | xml_parsed_output['vrf'][vrf]['rpm_handle_count'] = \ 341 | int(vrf_tree.find('{}rpm-handle-count'.format(namespace)).text) 342 | 343 | # route_map table 344 | rpm_tree = vrf_tree.find('{}TABLE_rmap'.format(namespace)) 345 | if not rpm_tree: 346 | continue 347 | 348 | # ----- loop route_map ----- 349 | for rmp_root in rpm_tree.findall('{}ROW_rmap'.format(namespace)): 350 | # route map 351 | try: 352 | name = rmp_root.find('{}name'.format(namespace)).text 353 | name = name.replace('>', '>') 354 | except: 355 | continue 356 | 357 | if 'route_map' not in xml_parsed_output['vrf'][vrf]: 358 | xml_parsed_output['vrf'][vrf]['route_map'] = {} 359 | 360 | if name not in xml_parsed_output['vrf'][vrf]['route_map']: 361 | xml_parsed_output['vrf'][vrf]['route_map'][name] = {} 362 | # initial index 363 | index = 1 364 | else: 365 | index += 1 366 | 367 | if index not in xml_parsed_output['vrf'][vrf]['route_map']\ 368 | [name]: 369 | xml_parsed_output['vrf'][vrf]['route_map'][name][index] = \ 370 | {} 371 | 372 | 373 | # deny 374 | try: 375 | xml_parsed_output['vrf'][vrf]['route_map'][name][index]\ 376 | ['action'] = rmp_root.\ 377 | find('{}action'.format(namespace)).text 378 | except: 379 | pass 380 | 381 | # 10 382 | try: 383 | xml_parsed_output['vrf'][vrf]['route_map'][name][index]\ 384 | ['seq_num'] = int(rmp_root.\ 385 | find('{}seqnum'.format(namespace)).text) 386 | except: 387 | pass 388 | 389 | # 0 390 | try: 391 | xml_parsed_output['vrf'][vrf]['route_map'][name][index]\ 392 | ['total_accept_count'] = int(rmp_root.\ 393 | find('{}totalacceptcount'.format(namespace)).text) 394 | except: 395 | pass 396 | 397 | # 2 398 | try: 399 | xml_parsed_output['vrf'][vrf]['route_map'][name][index]\ 400 | ['total_reject_count'] = int(rmp_root.\ 401 | find('{}totalrejectcount'.format(namespace)).text) 402 | except: 403 | pass 404 | 405 | 406 | # TABLE_cmd table 407 | command = rmp_root.find('{}TABLE_cmd'.format(namespace)) 408 | 409 | if not command: 410 | continue 411 | 412 | # ----- loop command ----- 413 | for command_root in command.findall('{}ROW_cmd'.\ 414 | format(namespace)): 415 | try: 416 | cmd_str = command_root.find('{}command'.\ 417 | format(namespace)).text.strip() 418 | cmd_str = cmd_str.replace('>', '>') 419 | except: 420 | continue 421 | 422 | if 'command' not in xml_parsed_output['vrf'][vrf]\ 423 | ['route_map'][name][index]: 424 | xml_parsed_output['vrf'][vrf]['route_map'][name]\ 425 | [index]['command'] = {} 426 | 427 | # command 428 | xml_parsed_output['vrf'][vrf]['route_map'][name][index]\ 429 | ['command']['command'] = cmd_str 430 | 431 | # 2 432 | try: 433 | xml_parsed_output['vrf'][vrf]['route_map'][name][index]\ 434 | ['command']['compare_count'] = \ 435 | int(command_root.find('{}comparecount'.\ 436 | format(namespace)).text) 437 | except: 438 | pass 439 | 440 | # 0 441 | try: 442 | xml_parsed_output['vrf'][vrf]['route_map'][name][index]\ 443 | ['command']['match_count'] = \ 444 | int(command_root.find('{}matchcount'.\ 445 | format(namespace)).text) 446 | except: 447 | pass 448 | pprint.pprint(xml_parsed_output) 449 | 450 | 451 | # Instantiate the parser class 452 | My_object = Parser(device=None) 453 | 454 | # Call the cli function of the class to print the cli parsed output 455 | My_object.cli() 456 | 457 | # Call the xml function of the class to print the xml parsed output 458 | My_object.xml() 459 | 460 | 461 | # Parsed outputs 462 | # ============== 463 | 464 | # cli_parsed_output = { 465 | # "vrf": { 466 | # "default": { 467 | # "route_map": { 468 | # "ADD_RT_400_400": { 469 | # 1: { 470 | # "seq_num": 10, 471 | # "action": "permit", 472 | # "total_reject_count": 0, 473 | # "total_accept_count": 0 474 | # } 475 | # } 476 | # }, 477 | # "rpm_handle_count": 1 478 | # }, 479 | # "vpn2": { 480 | # "rpm_handle_count": 0 481 | # }, 482 | # "vpn1": { 483 | # "route_map": { 484 | # "PERMIT_ALL_RM": { 485 | # 1: { 486 | # "seq_num": 20, 487 | # "action": "permit", 488 | # "total_reject_count": 0, 489 | # "total_accept_count": 0 490 | # } 491 | # } 492 | # }, 493 | # "rpm_handle_count": 1 494 | # }, 495 | # "ac": { 496 | # "rpm_handle_count": 0 497 | # } 498 | # } 499 | # } 500 | 501 | # xml_parsed_output = { 502 | # "vrf": { 503 | # "default": { 504 | # "route_map": { 505 | # "ADD_RT_400_400": { 506 | # 1: { 507 | # "seq_num": 10, 508 | # "action": "permit", 509 | # "total_reject_count": 0, 510 | # "total_accept_count": 0 511 | # } 512 | # } 513 | # }, 514 | # "rpm_handle_count": 1 515 | # }, 516 | # "vpn2": { 517 | # "rpm_handle_count": 0 518 | # }, 519 | # "vpn1": { 520 | # "route_map": { 521 | # "PERMIT_ALL_RM": { 522 | # 1: { 523 | # "seq_num": 20, 524 | # "action": "permit", 525 | # "total_reject_count": 0, 526 | # "total_accept_count": 0 527 | # } 528 | # } 529 | # }, 530 | # "rpm_handle_count": 1 531 | # }, 532 | # "ac": { 533 | # "rpm_handle_count": 0 534 | # } 535 | # } 536 | # } --------------------------------------------------------------------------------